2022-01-07 15:58:30 +01:00
import { ApprovalStatus , ConversationType , MailFolderType , MailMethod , MAX_ATTACHMENT_SIZE , OperationType , ReplyType } from "../../api/common/TutanotaConstants"
2021-02-03 17:13:38 +01:00
import type { RecipientInfo } from "../../api/common/RecipientInfo"
2021-08-19 14:12:10 +02:00
import { isExternal , makeRecipientDetails } from "../../api/common/RecipientInfo"
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"
2021-11-04 14:05:23 +01:00
import type { lazy } from "@tutao/tutanota-utils"
2022-01-07 15:58:30 +01:00
import { cleanMatch , deduplicate , downcast , getFromMap , neverNull , noOp , ofClass , promiseMap , remove , typedValues } from "@tutao/tutanota-utils"
2021-12-28 13:53:11 +01:00
import {
checkAttachmentSize ,
createRecipientInfo ,
getDefaultSender ,
getEnabledMailAddressesWithUser ,
getSenderNameForUser ,
getTemplateLanguages ,
2022-01-07 15:58:30 +01:00
RecipientField ,
2021-12-28 13:53:11 +01:00
recipientInfoToDraftRecipient ,
recipientInfoToEncryptedMailAddress ,
resolveRecipientInfo ,
resolveRecipientInfoContact ,
2021-02-03 17:13:38 +01:00
} from "../model/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"
import { FileNotFoundError } from "../../api/common/error/FileNotFoundError"
import type { LoginController } from "../../api/main/LoginController"
import { logins } from "../../api/main/LoginController"
import type { MailAddress } from "../../api/entities/tutanota/MailAddress"
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"
2021-02-03 17:13:38 +01:00
import type { EntityEventsListener , EntityUpdateData } from "../../api/main/EventController"
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"
import type { ContactModel } from "../../contacts/model/ContactModel"
2021-05-27 15:14:41 +02:00
import type { Language , TranslationKey , TranslationText } from "../../misc/LanguageViewModel"
2021-02-03 17:13:38 +01:00
import { _getSubstitutedLanguageCode , getAvailableLanguageCode , lang , languages } from "../../misc/LanguageViewModel"
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"
2021-04-15 08:57:52 +02:00
import { CustomerPropertiesTypeRef } from "../../api/entities/sys/CustomerProperties"
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"
2019-08-22 18:24:32 +02:00
assertMainOrNode ( )
2021-12-23 14:03:23 +01:00
export const TOO_MANY_VISIBLE_RECIPIENTS = 10
export type Recipient = {
2021-12-28 13:53:11 +01:00
name : string | null
address : string
contact? : Contact | null
2021-12-23 14:03:23 +01:00
}
export type RecipientList = ReadonlyArray < Recipient >
export type Recipients = {
2021-12-28 13:53:11 +01:00
to? : RecipientList
cc? : RecipientList
bcc? : RecipientList
2021-12-23 14:03:23 +01:00
}
2021-12-28 13:53:11 +01:00
2021-12-23 14:03:23 +01:00
export function makeRecipient ( address : string , name : string | null , contact : Contact | null ) : Recipient {
2021-12-28 13:53:11 +01:00
return {
name ,
address ,
contact ,
}
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 makeRecipients ( to : RecipientList , cc : RecipientList , bcc : RecipientList ) : Recipients {
2021-12-28 13:53:11 +01:00
return {
to ,
cc ,
bcc ,
}
2020-09-18 14:41:31 +02:00
}
2021-12-28 13:53:11 +01:00
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 {
2021-12-28 13:53:11 +01:00
return {
name ,
address ,
}
2019-08-22 18:24:32 +02:00
}
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
2020-09-18 14:41:31 +02:00
export type ResponseMailParameters = {
2021-12-28 13:53:11 +01:00
previousMail : Mail
conversationType : ConversationType
senderMailAddress : string
toRecipients : MailAddress [ ]
ccRecipients : MailAddress [ ]
bccRecipients : MailAddress [ ]
attachments : TutanotaFile [ ]
subject : string
bodyText : string
replyTos : EncryptedMailAddress [ ]
2020-09-18 14:41:31 +02:00
}
2019-08-22 18:24:32 +02:00
2021-06-25 16:14:54 +02:00
/ * *
* Simple blocking wait handler implementation which just ignores messages and can be used to silently save a draft without showing progress .
* /
export function noopBlockingWaitHandler < T > ( messageIdOrMessageFunction : TranslationKey | lazy < string > , action : Promise < T > ) : Promise < T > {
2021-12-28 13:53:11 +01:00
return action
2021-06-25 16:14:54 +02: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-01-13 11:57:55 +01:00
private _mailFacade : MailFacade
private _entity : EntityClient
private _logins : LoginController
private _mailModel : MailModel
private _contactModel : ContactModel
private _eventController : EventController
private _mailboxDetails : MailboxDetail
private _conversationType : ConversationType
private _subject : string // we're setting subject to the value of the subject TextField in the MailEditor
2021-12-28 13:53:11 +01:00
2022-01-13 11:57:55 +01:00
private _body : string
// Isn't private because used by MinimizedEditorOverlay, refactor?
2022-02-04 17:20:32 +01:00
draft : Mail | null
2022-01-13 11:57:55 +01:00
private _recipients : Map < RecipientField , Array < RecipientInfo > >
private _senderAddress : string
private _isConfidential : boolean
private _attachments : Array < Attachment > // contains either Files from Tutanota or DataFiles of locally loaded files. these map 1:1 to the _attachmentButtons
2021-12-28 13:53:11 +01:00
2022-01-13 11:57:55 +01:00
private _replyTos : Array < RecipientInfo >
private _previousMessageId : Id | null // only needs to be the correct value if this is a new email. if we are editing a draft, conversationType is not used
2021-12-28 13:53:11 +01:00
2022-01-13 11:57:55 +01:00
private _previousMail : Mail | null
private _selectedNotificationLanguage : string
private _availableNotificationTemplateLanguages ! : Array < Language >
private _entityEventReceived : EntityEventsListener
private _mailChanged : boolean
private _passwords : Map < string , string >
2021-12-28 13:53:11 +01:00
onMailChanged : Stream < boolean >
2022-01-07 15:58:30 +01:00
onRecipientDeleted : Stream < { field : RecipientField , recipient : RecipientInfo } | null >
2021-12-28 13:53:11 +01:00
onBeforeSend : ( ) = > unknown
loadedInlineImages : InlineImages
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-01-07 15:58:30 +01:00
mailFacade : MailFacade ,
logins : LoginController ,
mailModel : MailModel ,
contactModel : ContactModel ,
eventController : EventController ,
entity : EntityClient ,
mailboxDetails : MailboxDetail ,
2021-12-28 13:53:11 +01:00
) {
this . _mailFacade = mailFacade
this . _entity = entity
this . _logins = logins
this . _mailModel = mailModel
this . _contactModel = contactModel
this . _eventController = eventController
this . _mailboxDetails = mailboxDetails
const userProps = logins . getUserController ( ) . props
this . _conversationType = ConversationType . NEW
this . _subject = ""
this . _body = ""
2022-02-04 17:20:32 +01:00
this . draft = null
2021-12-28 13:53:11 +01:00
this . _recipients = new Map ( )
this . _senderAddress = this . _getDefaultSender ( )
this . _isConfidential = ! userProps . defaultUnconfidential
this . _attachments = [ ]
this . _replyTos = [ ]
this . _previousMessageId = null
this . _previousMail = null
this . _selectedNotificationLanguage = getAvailableLanguageCode ( userProps . notificationMailLanguage || lang . code )
this . updateAvailableNotificationTemplateLanguages ( )
this . _entityEventReceived = updates = > {
return promiseMap ( updates , update = > {
return this . _handleEntityEvent ( update )
} ) . then ( noOp )
}
this . _eventController . addEntityListener ( this . _entityEventReceived )
this . _passwords = new Map ( )
this . _mailChanged = false
this . onMailChanged = stream ( false )
this . onRecipientDeleted = stream ( null )
this . onBeforeSend = noOp
this . loadedInlineImages = new Map ( )
}
/ * *
* 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
* /
updateAvailableNotificationTemplateLanguages ( ) : Promise < void > {
this . _availableNotificationTemplateLanguages = languages . slice ( ) . sort ( ( a , b ) = > lang . get ( a . textId ) . localeCompare ( lang . get ( b . textId ) ) )
return getTemplateLanguages ( this . _availableNotificationTemplateLanguages , this . _entity , this . _logins ) . then ( filteredLanguages = > {
if ( filteredLanguages . length > 0 ) {
const languageCodes = filteredLanguages . map ( l = > l . code )
this . _selectedNotificationLanguage =
2022-01-07 15:58:30 +01:00
_getSubstitutedLanguageCode ( this . _logins . getUserController ( ) . props . notificationMailLanguage || lang . code , languageCodes ) || languageCodes [ 0 ]
2021-12-28 13:53:11 +01:00
this . _availableNotificationTemplateLanguages = filteredLanguages
}
} )
}
logins ( ) : LoginController {
return this . _logins
}
user ( ) : IUserController {
return this . logins ( ) . getUserController ( )
}
contacts ( ) : ContactModel {
return this . _contactModel
}
mails ( ) : MailModel {
return this . _mailModel
}
mailFacade ( ) : MailFacade {
return this . _mailFacade
}
events ( ) : EventController {
return this . _eventController
}
entity ( ) : EntityClient {
return this . _entity
}
getPreviousMail ( ) : Mail | null {
return this . _previousMail
}
getMailboxDetails ( ) : MailboxDetail {
return this . _mailboxDetails
}
getConversationType ( ) : ConversationType {
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
}
setSubject ( subject : string ) {
this . _mailChanged = subject !== this . _subject
this . _subject = subject
}
getBody ( ) : string {
return this . _body
}
setBody ( body : string ) {
this . _body = body
this . setMailChanged ( true )
}
setSender ( senderAddress : string ) {
this . _senderAddress = senderAddress
this . setMailChanged ( true )
}
getSender ( ) : string {
return this . _senderAddress
}
/ * *
* 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 (
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 > {
return this . _init ( {
conversationType : ConversationType.NEW ,
subject ,
bodyText ,
recipients ,
attachments ,
confidential ,
senderMailAddress ,
} )
}
2022-04-07 17:00:54 +02:00
async initAsResponse ( args : ResponseMailParameters , inlineImages : InlineImages ) : Promise < SendMailModel > {
2021-12-28 13:53:11 +01:00
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 ) ,
}
let previousMessageId : string | null = null
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 )
2021-12-28 13:53:11 +01:00
return this . _init ( {
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
const conversationEntry = await this . _entity . load ( ConversationEntryTypeRef , draft . conversationEntry )
const conversationType = downcast < ConversationType > ( conversationEntry . conversationType )
if ( conversationEntry . previous ) {
try {
const previousEntry = await this . _entity . load ( ConversationEntryTypeRef , conversationEntry . previous )
previousMessageId = previousEntry . messageId
if ( previousEntry . mail ) {
previousMail = await this . _entity . load ( MailTypeRef , previousEntry . mail )
}
} 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 = {
to : toRecipients.map ( mailAddressToRecipient ) ,
cc : ccRecipients.map ( mailAddressToRecipient ) ,
bcc : bccRecipients.map ( mailAddressToRecipient ) ,
}
return this . _init ( {
conversationType : conversationType ,
subject ,
bodyText ,
recipients ,
draft ,
senderMailAddress : sender.address ,
confidential ,
attachments ,
replyTos ,
previousMail ,
previousMessageId ,
} )
}
_init ( {
conversationType ,
subject ,
bodyText ,
draft ,
recipients ,
senderMailAddress ,
confidential ,
attachments ,
replyTos ,
previousMail ,
previousMessageId ,
} : {
conversationType : ConversationType
subject : string
bodyText : string
recipients : Recipients
2022-01-07 15:58:30 +01:00
confidential : boolean | null | undefined
draft? : Mail | null | undefined
2021-12-28 13:53:11 +01:00
senderMailAddress? : string
attachments? : ReadonlyArray < Attachment >
replyTos? : EncryptedMailAddress [ ]
2022-01-07 15:58:30 +01:00
previousMail? : Mail | null | undefined
previousMessageId? : string | null | undefined
2021-12-28 13:53:11 +01:00
} ) : 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
const { to = [ ] , cc = [ ] , bcc = [ ] } = recipients
const makeRecipientInfo = ( r : Recipient ) = > {
const [ recipient ] = this . _createAndResolveRecipientInfo ( r . name , r . address , r . contact , false )
if ( recipient . resolveContactPromise ) {
recipient . resolveContactPromise . then ( ( ) = > ( this . _mailChanged = false ) )
} else {
this . _mailChanged = false
}
return recipient
}
const recipientsTransform = ( recipientList : RecipientList ) = > {
return deduplicate (
2022-01-07 15:58:30 +01:00
recipientList . filter ( r = > isMailAddress ( r . address , false ) ) ,
( a , b ) = > a . address === b . address ,
2021-12-28 13:53:11 +01:00
) . map ( makeRecipientInfo )
}
2022-01-07 15:58:30 +01:00
this . _recipients . set ( RecipientField . TO , recipientsTransform ( to ) )
2021-12-28 13:53:11 +01:00
2022-01-07 15:58:30 +01:00
this . _recipients . set ( RecipientField . CC , recipientsTransform ( cc ) )
2021-12-28 13:53:11 +01:00
2022-01-07 15:58:30 +01:00
this . _recipients . set ( RecipientField . BCC , recipientsTransform ( bcc ) )
2021-12-28 13:53:11 +01:00
this . _senderAddress = senderMailAddress || this . _getDefaultSender ( )
this . _isConfidential = confidential == null ? ! this . user ( ) . props.defaultUnconfidential : confidential
this . _attachments = [ ]
if ( attachments ) {
this . attachFiles ( attachments )
this . _mailChanged = false
}
this . _replyTos = ( replyTos || [ ] ) . map ( ema = > {
const ri = createRecipientInfo ( ema . address , ema . name , null )
if ( this . _logins . isInternalUserLoggedIn ( ) ) {
resolveRecipientInfoContact ( ri , this . _contactModel , this . user ( ) . user ) . then ( ( ) = > {
this . onMailChanged ( true )
} )
}
return ri
} )
this . _previousMail = previousMail || null
this . _previousMessageId = previousMessageId || null
this . _mailChanged = false
return Promise . resolve ( this )
}
_getDefaultSender ( ) : string {
return getDefaultSender ( this . _logins , this . _mailboxDetails )
}
getRecipientList ( type : RecipientField ) : Array < RecipientInfo > {
return getFromMap ( this . _recipients , type , ( ) = > [ ] )
}
toRecipients ( ) : Array < RecipientInfo > {
2022-01-07 15:58:30 +01:00
return this . getRecipientList ( RecipientField . TO )
2021-12-28 13:53:11 +01:00
}
ccRecipients ( ) : Array < RecipientInfo > {
2022-01-07 15:58:30 +01:00
return this . getRecipientList ( RecipientField . CC )
2021-12-28 13:53:11 +01:00
}
bccRecipients ( ) : Array < RecipientInfo > {
2022-01-07 15:58:30 +01:00
return this . getRecipientList ( RecipientField . BCC )
2021-12-28 13:53:11 +01: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 }
* /
addOrGetRecipient ( type : RecipientField , recipient : Recipient , skipResolveContact : boolean = false ) : [ RecipientInfo , Promise < RecipientInfo > ] {
// 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 || "" )
}
// make a new recipient info if we don't have one for that recipient
if ( ! recipientInfo ) {
let p : Promise < RecipientInfo >
; [ recipientInfo , p ] = this . _createAndResolveRecipientInfo ( recipient . name , recipient . address , recipient . contact , skipResolveContact )
this . getRecipientList ( type ) . push ( recipientInfo )
this . setMailChanged ( true )
return [ recipientInfo , p ]
} else {
return [ recipientInfo , Promise . resolve ( recipientInfo ) ]
}
}
_createAndResolveRecipientInfo (
2022-01-07 15:58:30 +01:00
name : string | null ,
address : string ,
contact : Contact | null | undefined ,
skipResolveContact : boolean ,
2021-12-28 13:53:11 +01:00
) : [ RecipientInfo , Promise < RecipientInfo > ] {
2022-01-07 15:58:30 +01:00
const ri = createRecipientInfo ( address , name , contact ? ? null )
2021-12-28 13:53:11 +01:00
let p : Promise < RecipientInfo >
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 || "" )
}
} )
}
p = resolveRecipientInfo ( this . _mailFacade , ri ) . then ( resolved = > {
this . setMailChanged ( true )
return resolved
} )
} else {
p = Promise . resolve ( ri )
}
return [ ri , p ]
}
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
}
dispose() {
this . _eventController . removeEntityListener ( this . _entityEventReceived )
revokeInlineImages ( this . loadedInlineImages )
}
/ * *
* @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
}
/** @throws UserError in case files are too big to add */
attachFiles ( files : ReadonlyArray < Attachment > ) : void {
let sizeLeft = MAX_ATTACHMENT_SIZE - this . _attachments . reduce ( ( total , file ) = > total + Number ( file . size ) , 0 )
const sizeCheckResult = checkAttachmentSize ( files , sizeLeft )
this . _attachments . push ( . . . sizeCheckResult . attachableFiles )
this . setMailChanged ( true )
if ( sizeCheckResult . tooBigFiles . length > 0 ) {
throw new UserError ( ( ) = > lang . get ( "tooBigAttachment_msg" ) + "\n" + sizeCheckResult . tooBigFiles . join ( "\n" ) )
}
}
removeAttachment ( file : Attachment ) : void {
if ( remove ( this . _attachments , file ) ) {
this . setMailChanged ( true )
}
}
getSenderName ( ) : string {
return getSenderNameForUser ( this . _mailboxDetails , this . user ( ) )
}
getDraft ( ) : Readonly < Mail > | null {
2022-02-04 17:20:32 +01:00
return this . draft
2021-12-28 13:53:11 +01:00
}
_updateDraft ( body : string , attachments : ReadonlyArray < Attachment > | null , draft : Mail ) : Promise < Mail > {
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 ,
senderMailAddress : this._senderAddress ,
senderName : this.getSenderName ( ) ,
toRecipients : this.toRecipients ( ) . map ( recipientInfoToDraftRecipient ) ,
ccRecipients : this.ccRecipients ( ) . map ( recipientInfoToDraftRecipient ) ,
bccRecipients : this.bccRecipients ( ) . map ( recipientInfoToDraftRecipient ) ,
attachments : attachments ,
confidential : this.isConfidential ( ) ,
draft : draft
} ,
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" )
return this . _createDraft ( body , attachments , downcast ( draft . method ) )
} ) ,
)
2021-12-28 13:53:11 +01:00
}
_createDraft ( body : string , attachments : ReadonlyArray < Attachment > | null , mailMethod : MailMethod ) : Promise < Mail > {
return this . _mailFacade . createDraft (
2022-02-03 14:55:40 +01:00
{
subject : this.getSubject ( ) ,
bodyText : body ,
senderMailAddress : this._senderAddress ,
senderName : this.getSenderName ( ) ,
toRecipients : this.toRecipients ( ) . map ( recipientInfoToDraftRecipient ) ,
ccRecipients : this.ccRecipients ( ) . map ( recipientInfoToDraftRecipient ) ,
bccRecipients : this.bccRecipients ( ) . map ( recipientInfoToDraftRecipient ) ,
conversationType : this._conversationType ,
previousMessageId : this._previousMessageId ,
attachments : attachments ,
confidential : this.isConfidential ( ) ,
replyTos : this._replyTos.map ( recipientInfoToEncryptedMailAddress ) ,
method : mailMethod
} ,
2021-12-28 13:53:11 +01:00
)
}
isConfidential ( ) : boolean {
return this . _isConfidential || ! this . containsExternalRecipients ( )
}
isConfidentialExternal ( ) : boolean {
return this . _isConfidential && this . containsExternalRecipients ( )
}
setConfidential ( confidential : boolean ) : void {
this . _isConfidential = confidential
}
containsExternalRecipients ( ) : boolean {
return this . allRecipients ( ) . some ( r = > isExternal ( r ) )
}
getExternalRecipients ( ) : Array < RecipientInfo > {
return this . allRecipients ( ) . filter ( r = > isExternal ( r ) )
}
/ * *
* @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 ( )
if ( this . allRecipients ( ) . length === 1 && this . allRecipients ( ) [ 0 ] . mailAddress . toLowerCase ( ) . trim ( ) === "approval@tutao.de" ) {
await this . _sendApprovalMail ( this . getBody ( ) )
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
await this . waitForResolvedRecipients ( )
// No password in external confidential mail is an error
if ( this . isConfidentialExternal ( ) && this . getExternalRecipients ( ) . some ( r = > ! this . getPassword ( r . mailAddress ) ) ) {
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 )
2021-12-28 13:53:11 +01:00
await this . _updateContacts ( this . allRecipients ( ) )
const allRecipients = this . allRecipients ( ) . map ( ( {
name ,
mailAddress ,
type ,
contact
} ) = > makeRecipientDetails ( name , mailAddress , type , contact ) )
2022-02-04 17:20:32 +01:00
await this . _mailFacade . sendDraft ( neverNull ( this . draft ) , allRecipients , this . _selectedNotificationLanguage )
2021-12-28 13:53:11 +01:00
await this . _updatePreviousMail ( )
await this . _updateExternalLanguage ( )
return true
}
return waitHandler ( this . isConfidential ( ) ? "sending_msg" : "sendingUnencrypted_msg" , doSend ( ) )
2022-01-07 15:58:30 +01:00
. catch (
ofClass ( LockedError , ( ) = > {
2022-02-04 17:20:32 +01:00
throw new UserError ( "operationStillActive_msg" )
2022-01-07 15:58:30 +01:00
} ) ,
) // catch all of the badness
. catch (
ofClass ( RecipientNotResolvedError , ( ) = > {
2022-02-04 17:20:32 +01:00
throw new UserError ( "tooManyAttempts_msg" )
2022-01-07 15:58:30 +01:00
} ) ,
)
. catch (
ofClass ( RecipientsNotFoundError , e = > {
2022-02-04 17:20:32 +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-02-04 17:20:32 +01:00
throw new UserError ( tooManyRequestsError )
2022-01-07 15:58:30 +01:00
} ) ,
)
. catch (
ofClass ( AccessBlockedError , e = > {
2022-02-04 17:20:32 +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-02-04 17:20:32 +01:00
throw new UserError ( "couldNotAttachFile_msg" )
2022-01-07 15:58:30 +01:00
} ) ,
)
. catch (
ofClass ( PreconditionFailedError , ( ) = > {
2022-02-04 17:20:32 +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-01-07 15:58:30 +01:00
. filter ( r = > this . getPassword ( r . mailAddress ) !== "" )
. 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
}
if ( this . _mailChanged && this . doSaveAgain ) {
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
this . _mailChanged = false
try {
const attachments = saveAttachments ? this . _attachments : null
// 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 )
? await this . _createDraft ( this . getBody ( ) , attachments , mailMethod )
: await this . _updateDraft ( this . getBody ( ) , attachments , this . draft )
const newAttachments = await promiseMap (
this . draft . attachments ,
fileId = > this . _entity . load < TutanotaFile > ( FileTypeRef , fileId ) ,
{
concurrency : 5 ,
}
2022-01-07 15:58:30 +01:00
)
2022-02-04 17:20:32 +01:00
this . _attachments = [ ] // attachFiles will push to existing files but we want to overwrite them
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 ) {
const folders = await this . _mailModel . getMailboxFolders ( draft )
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-01-07 15:58:30 +01:00
_sendApprovalMail ( body : string ) : Promise < unknown > {
2021-12-28 13:53:11 +01:00
const listId = "---------c--"
const m = createApprovalMail ( {
_id : [ listId , stringToCustomId ( this . _senderAddress ) ] ,
_ownerGroup : this.user ( ) . user . userGroup . group ,
text : ` Subject: ${ this . getSubject ( ) } <br> ${ body } ` ,
} )
return this . _entity . setup ( listId , m ) . catch ( ofClass ( 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 )
}
_updateExternalLanguage() {
let props = this . user ( ) . props
if ( props . notificationMailLanguage !== this . _selectedNotificationLanguage ) {
props . notificationMailLanguage = this . _selectedNotificationLanguage
this . _entity . update ( props )
}
}
_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 ( )
}
return this . _entity . update ( this . _previousMail ) . catch (
2022-01-07 15:58:30 +01:00
ofClass ( NotFoundError , e = > {
// ignore
} ) ,
2021-12-28 13:53:11 +01:00
)
} else {
return Promise . resolve ( )
}
}
_updateContacts ( resolvedRecipients : RecipientInfo [ ] ) : Promise < any > {
return Promise . all (
2022-01-07 15:58:30 +01:00
resolvedRecipients . map ( r = > {
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 ( )
2021-12-28 13:53:11 +01:00
}
2022-01-07 15:58:30 +01:00
return this . _contactModel . contactListId ( ) . 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 )
} else {
return Promise . resolve ( )
}
} ) ,
2021-12-28 13:53:11 +01:00
)
}
allRecipients ( ) : Array < RecipientInfo > {
return this . toRecipients ( ) . concat ( this . ccRecipients ( ) ) . concat ( this . bccRecipients ( ) )
}
/ * *
* Makes sure the recipient type and contact are resolved .
* /
waitForResolvedRecipients ( ) : Promise < RecipientInfo [ ] > {
return Promise . all (
2022-01-07 15:58:30 +01:00
this . allRecipients ( ) . map ( recipientInfo = > {
return resolveRecipientInfo ( this . _mailFacade , recipientInfo ) . then ( recipientInfo = > {
if ( recipientInfo . resolveContactPromise ) {
return recipientInfo . resolveContactPromise . then ( ( ) = > recipientInfo )
} else {
return recipientInfo
}
} )
} ) ,
2021-12-28 13:53:11 +01:00
) . catch (
2022-01-07 15:58:30 +01:00
ofClass ( TooManyRequestsError , ( ) = > {
throw new RecipientNotResolvedError ( "" )
} ) ,
2021-12-28 13:53:11 +01:00
)
}
_handleEntityEvent ( update : EntityUpdateData ) : Promise < void > {
const { operation , instanceId , instanceListId } = update
let contactId : IdTuple = [ neverNull ( instanceListId ) , instanceId ]
if ( isUpdateForTypeRef ( ContactTypeRef , update ) ) {
if ( operation === OperationType . UPDATE ) {
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
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 ) {
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 {
2021-12-28 13:53:11 +01:00
return new SendMailModel ( locator . mailFacade , logins , locator . mailModel , locator . contactModel , locator . eventController , locator . entityClient , mailboxDetails )
2021-05-27 15:14:41 +02:00
}