Splits MailModel into two, adding MailboxModel

close pi#214

Calendar only needs access to MailboxModel, MailModel handles deleting
and moving mails
This commit is contained in:
wrd 2024-08-07 16:29:40 +02:00 committed by and
parent f914cb2613
commit 88355bd5d1
65 changed files with 989 additions and 727 deletions

View file

@ -8,7 +8,7 @@ export const ansiSequences = Object.freeze({
faint: "\x1b[0;2m",
})
type CodeValues = (typeof ansiSequences)[keyof typeof ansiSequences]
type CodeValues = typeof ansiSequences[keyof typeof ansiSequences]
export function fancy(text: string, code: CodeValues) {
if (typeof process !== "undefined" && process.stdout.isTTY) {

View file

@ -9,8 +9,8 @@ import { createFile } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { convertToDataFile, DataFile } from "../../../common/api/common/DataFile"
import type { DateWrapper, RepeatRule, UserAlarmInfo } from "../../../common/api/entities/sys/TypeRefs.js"
import { DateTime } from "luxon"
import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController"
import { getLetId } from "../../../common/api/common/utils/EntityUtils"
import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController.js"
/** create an ical data file that can be attached to an invitation/update/cancellation/response mail */
export function makeInvitationCalendarFile(event: CalendarEvent, method: CalendarMethod, now: Date, zone: string): DataFile {

View file

@ -65,7 +65,7 @@ import {
MailboxProperties,
} from "../../../../common/api/entities/tutanota/TypeRefs.js"
import { User } from "../../../../common/api/entities/sys/TypeRefs.js"
import { MailboxDetail } from "../../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../../../common/mailFunctionality/MailboxModel.js"
import {
AlarmInterval,
areRepeatRulesEqual,

View file

@ -48,7 +48,7 @@ import { ProgressTracker } from "../../../common/api/main/ProgressTracker"
import type { IProgressMonitor } from "../../../common/api/common/utils/ProgressMonitor"
import { NoopProgressMonitor } from "../../../common/api/common/utils/ProgressMonitor"
import { EntityClient } from "../../../common/api/common/EntityClient"
import type { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { elementIdPart, getElementId, isSameId, listIdPart, removeTechnicalFields } from "../../../common/api/common/utils/EntityUtils"
import type { AlarmScheduler } from "../../../common/calendar/date/AlarmScheduler.js"
import { Notifications, NotificationType } from "../../../common/gui/Notifications"
@ -134,7 +134,7 @@ export class CalendarModel {
private readonly logins: LoginController,
private readonly progressTracker: ProgressTracker,
private readonly entityClient: EntityClient,
private readonly mailModel: MailModel,
private readonly mailboxModel: MailboxModel,
private readonly calendarFacade: CalendarFacade,
private readonly fileController: FileController,
private readonly zone: string,
@ -454,7 +454,7 @@ export class CalendarModel {
}
private async loadAndProcessCalendarUpdates(): Promise<void> {
const { mailboxGroupRoot } = await this.mailModel.getUserMailboxDetails()
const { mailboxGroupRoot } = await this.mailboxModel.getUserMailboxDetails()
const { calendarEventUpdates } = mailboxGroupRoot
if (calendarEventUpdates == null) return

View file

@ -470,8 +470,8 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
await showProgressDialog("pleaseWait_msg", calendarInfos)
}
const mailboxDetails = await calendarLocator.mailModel.getUserMailboxDetails()
const mailboxProperties = await calendarLocator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxDetails = await calendarLocator.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await calendarLocator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await calendarLocator.calendarEventModel(
CalendarOperation.Create,
getEventWithDefaultTimes(dateToUse),

View file

@ -17,7 +17,7 @@ import { isCustomizationEnabledForCustomer } from "../../../common/api/common/ut
import { getEventType } from "../gui/CalendarGuiUtils.js"
import { CalendarModel } from "../model/CalendarModel.js"
import { LoginController } from "../../../common/api/main/LoginController.js"
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { SendMailModel } from "../../../common/mailFunctionality/SendMailModel.js"
import { RecipientField } from "../../../common/mailFunctionality/SharedMailUtils.js"
@ -70,10 +70,10 @@ export async function showEventDetails(event: CalendarEvent, eventBubbleRect: Cl
} else {
const [calendarInfos, mailboxDetails, customer] = await Promise.all([
(await locator.calendarModel()).getCalendarInfos(),
locator.mailModel.getUserMailboxDetails(),
locator.mailboxModel.getUserMailboxDetails(),
locator.logins.getUserController().loadCustomer(),
])
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const ownMailAddresses = mailboxProperties.mailAddressProperties.map(({ mailAddress }) => mailAddress)
ownAttendee = findAttendeeInAddresses(latestEvent.attendees, ownMailAddresses)
eventType = getEventType(latestEvent, calendarInfos, ownMailAddresses, locator.logins.getUserController())
@ -138,7 +138,7 @@ export const enum ReplyResult {
export class CalendarInviteHandler {
constructor(
private readonly mailModel: MailModel,
private readonly mailboxModel: MailboxModel,
private readonly calendarModel: CalendarModel,
private readonly logins: LoginController,
private readonly calendarNotificationSender: CalendarNotificationSender,
@ -157,13 +157,17 @@ export class CalendarInviteHandler {
attendee: CalendarEventAttendee,
decision: CalendarAttendeeStatus,
previousMail: Mail,
mailboxDetails: MailboxDetail,
): Promise<ReplyResult> {
const eventClone = clone(event)
const foundAttendee = assertNotNull(findAttendeeInAddresses(eventClone.attendees, [attendee.address.address]), "attendee was not found in event clone")
foundAttendee.status = decision
const notificationModel = new CalendarNotificationModel(this.calendarNotificationSender, this.logins)
const responseModel = await this.getResponseModelForMail(previousMail, attendee.address.address)
//NOTE: mailDetails are getting passed through because the calendar does not have access to the mail folder structure
// which is needed to find mailboxdetails by mail. This may be fixed by static mail ids which are being worked on currently.
// This function is only called by EventBanner from the mail app so this should be okay.
const responseModel = await this.getResponseModelForMail(previousMail, mailboxDetails, attendee.address.address)
try {
await notificationModel.send(eventClone, [], { responseModel, inviteModel: null, cancelModel: null, updateModel: null })
@ -197,10 +201,10 @@ export class CalendarInviteHandler {
return ReplyResult.ReplySent
}
async getResponseModelForMail(previousMail: Mail, responder: string): Promise<SendMailModel | null> {
const mailboxDetails = await this.mailModel.getMailboxDetailsForMail(previousMail)
if (mailboxDetails == null) return null
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
async getResponseModelForMail(previousMail: Mail, mailboxDetails: MailboxDetail, responder: string): Promise<SendMailModel | null> {
//NOTE: mailDetails are getting passed through because the calendar does not have access to the mail folder structure
// which is needed to find mailboxdetails by mail. This may be fixed by static mail ids which are being worked on currently
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await this.sendMailModelFactory(mailboxDetails, mailboxProperties)
await model.initAsResponse(
{

View file

@ -598,8 +598,8 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
await showProgressDialog("pleaseWait_msg", calendarInfos)
}
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null)
if (model) {
await showNewCalendarEventEditDialog(model)

View file

@ -24,7 +24,7 @@ import { Time } from "../../../common/calendar/date/Time.js"
import { CalendarEventsRepository, DaysToEvents } from "../../../common/calendar/date/CalendarEventsRepository.js"
import { CalendarEventPreviewViewModel } from "../gui/eventpopup/CalendarEventPreviewViewModel.js"
import { EntityUpdateData, isUpdateFor, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { getEnabledMailAddressesWithUser } from "../../../common/mailFunctionality/SharedMailUtils.js"
export type EventsOnDays = {
@ -91,7 +91,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
private readonly deviceConfig: DeviceConfig,
private readonly calendarInvitationsModel: ReceivedGroupInvitationsModel<GroupType.Calendar>,
private readonly timeZone: string,
private readonly mailModel: MailModel,
private readonly mailboxModel: MailboxModel,
) {
this._transientEvents = []
@ -197,7 +197,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
private canFullyEditEvent(event: CalendarEvent): boolean {
const userController = this.logins.getUserController()
const userMailGroup = userController.getUserMailGroupMembership().group
const mailboxDetailsArray = this.mailModel.mailboxDetails()
const mailboxDetailsArray = this.mailboxModel.mailboxDetails()
const mailboxDetails = assertNotNull(mailboxDetailsArray.find((md) => md.mailGroup._id === userMailGroup))
const ownMailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userController.userGroupInfo)
const eventType = getEventType(event, this.calendarInfos, ownMailAddresses, userController)

View file

@ -1,6 +1,6 @@
import { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js"
import { EventController } from "../common/api/main/EventController.js"
import { MailboxDetail, MailModel } from "../common/mailFunctionality/MailModel.js"
import { type MailboxDetail, MailboxModel } from "../common/mailFunctionality/MailboxModel.js"
import { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
import { EntityClient } from "../common/api/common/EntityClient.js"
import { ProgressTracker } from "../common/api/main/ProgressTracker.js"
@ -110,13 +110,14 @@ import { AppType } from "../common/misc/ClientConstants.js"
import type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js"
import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js"
import { locator } from "../common/api/main/CommonLocator.js"
import m from "mithril"
assertMainOrNode()
class CalendarLocator {
eventController!: EventController
search!: CalendarSearchModel
mailModel!: MailModel
mailboxModel!: MailboxModel
contactModel!: ContactModel
entityClient!: EntityClient
progressTracker!: ProgressTracker
@ -269,8 +270,8 @@ class CalendarLocator {
return new CalendarViewModel(
this.logins,
async (mode: CalendarOperation, event: CalendarEvent) => {
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null)
},
(...args) => this.calendarEventPreviewModel(...args),
@ -282,7 +283,7 @@ class CalendarLocator {
deviceConfig,
await this.receivedGroupInvitationsModel(GroupType.Calendar),
timeZone,
this.mailModel,
this.mailboxModel,
)
})
@ -303,13 +304,16 @@ class CalendarLocator {
this.mailFacade,
this.entityClient,
this.logins,
this.mailModel,
this.mailboxModel,
this.contactModel,
this.eventController,
mailboxDetails,
recipientsModel,
dateProvider,
mailboxProperties,
async (mail: Mail) => {
return false
},
)
}
@ -425,7 +429,7 @@ class CalendarLocator {
async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> {
const { OwnMailAddressNameChanger } = await import("../mail-app/settings/mailaddress/OwnMailAddressNameChanger.js")
return new OwnMailAddressNameChanger(this.mailModel, this.entityClient)
return new OwnMailAddressNameChanger(this.mailboxModel, this.entityClient)
}
async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
@ -571,7 +575,7 @@ class CalendarLocator {
this.entropyFacade = entropyFacade
this.workerFacade = workerFacade
this.connectivityModel = new WebsocketConnectivityModel(eventBus)
this.mailModel = new MailModel(notifications, this.eventController, this.mailFacade, this.entityClient, this.logins, null, null)
this.mailboxModel = new MailboxModel(this.eventController, this.entityClient, this.logins)
this.operationProgressTracker = new OperationProgressTracker()
this.infoMessageHandler = new InfoMessageHandler((state: SearchIndexStateInfo) => {
// calendar does not have index, so nothing needs to be handled here
@ -607,18 +611,26 @@ class CalendarLocator {
const { WebInterWindowEventFacade } = await import("../common/native/main/WebInterWindowEventFacade.js")
const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js")
const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.js")
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailModel, CALENDAR_PREFIX)
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailboxModel, CALENDAR_PREFIX)
this.nativeInterfaces = createNativeInterfaces(
this.webMobileFacade,
new WebDesktopFacade(this.logins, async () => this.native),
new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig),
new WebCommonNativeFacade(
this.logins,
this.mailModel,
this.mailboxModel,
this.usageTestController,
async () => this.fileApp,
async () => this.pushService,
this.handleFileImport.bind(this),
async (userId: string, address: string, requestedPath: string | null) => noOp(),
async (userId: string) => {
if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) {
m.route.set("/calendar/agenda")
} else {
m.route.set(`/login?noAutoLogin=false&userId=${userId}&requestedPath=${encodeURIComponent("/calendar/agenda")}`)
}
},
AppType.Calendar,
),
cryptoFacade,
@ -763,7 +775,7 @@ class CalendarLocator {
this.logins,
this.progressTracker,
this.entityClient,
this.mailModel,
this.mailboxModel,
this.calendarFacade,
this.fileController,
timeZone,
@ -775,7 +787,7 @@ class CalendarLocator {
readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => {
const { CalendarInviteHandler } = await import("./calendar/view/CalendarInvites.js")
const { calendarNotificationSender } = await import("./calendar/view/CalendarNotificationSender.js")
return new CalendarInviteHandler(this.mailModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
return new CalendarInviteHandler(this.mailboxModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
this.sendMailModel(...arg),
)
})
@ -797,9 +809,9 @@ class CalendarLocator {
const { getEventType } = await import("./calendar/gui/CalendarGuiUtils.js")
const { CalendarEventPreviewViewModel } = await import("./calendar/gui/eventpopup/CalendarEventPreviewViewModel.js")
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const userController = this.logins.getUserController()
const customer = await userController.loadCustomer()

View file

@ -1201,3 +1201,4 @@ export enum GroupKeyRotationType {
export const GroupKeyRotationTypeNameByCode = reverse(GroupKeyRotationType)
export const EXTERNAL_CALENDAR_SYNC_INTERVAL = 60 * 30 * 1000 // 30 minutes

View file

@ -36,7 +36,7 @@ import { ExposedCacheStorage } from "../worker/rest/DefaultEntityRestCache.js"
import { WorkerFacade } from "../worker/facades/WorkerFacade.js"
import { WorkerRandomizer } from "../worker/WorkerImpl.js"
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js"
import { MailboxDetail, MailModel } from "../../mailFunctionality/MailModel.js"
import type { MailboxDetail, MailboxModel } from "../../mailFunctionality/MailboxModel.js"
import { EventController } from "./EventController.js"
import type { ContactModel } from "../../contactsFunctionality/ContactModel.js"
import { ProgressTracker } from "./ProgressTracker.js"
@ -115,7 +115,7 @@ export interface CommonLocator {
random: WorkerRandomizer
connectivityModel: WebsocketConnectivityModel
mailModel: MailModel
mailboxModel: MailboxModel
calendarModel(): Promise<CalendarModel>

View file

@ -55,7 +55,7 @@ import { FolderSystem } from "../../common/mail/FolderSystem.js"
import { AssociationType, Cardinality, Type as TypeId, ValueType } from "../../common/EntityConstants.js"
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
import { sql, SqlFragment } from "./Sql.js"
import { isDraft, isSpamOrTrashFolder } from "../../common/CommonMailUtils.js"
import { isDraft, isSpamOrTrashFolder } from "../../../../mail-app/mail/model/MailModel.js"
/**
* this is the value of SQLITE_MAX_VARIABLE_NUMBER in sqlite3.c

View file

@ -41,7 +41,8 @@ import { MailFacade } from "../facades/lazy/MailFacade.js"
import { containsEventOfType, EntityUpdateData } from "../../common/utils/EntityUpdateUtils.js"
import { b64UserIdHash } from "./DbFacade.js"
import { hasError } from "../../common/utils/ErrorUtils.js"
import { getDisplayedSender, getMailBodyText, isDraft, MailAddressAndName } from "../../common/CommonMailUtils.js"
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../common/CommonMailUtils.js"
import { isDraft } from "../../../../mail-app/mail/model/MailModel.js"
export const INITIAL_MAIL_INDEX_INTERVAL_DAYS = 28
const ENTITY_INDEXER_CHUNK = 20

View file

@ -3,7 +3,7 @@ import { CredentialEncryptionMode } from "../../misc/credentials/CredentialEncry
/** the single source of truth for this configuration */
export const SUPPORTED_MODES = Object.freeze([CredentialEncryptionMode.DEVICE_LOCK, CredentialEncryptionMode.APP_PASSWORD] as const)
export type DesktopCredentialsMode = (typeof SUPPORTED_MODES)[number]
export type DesktopCredentialsMode = typeof SUPPORTED_MODES[number]
export function assertSupportedEncryptionMode(encryptionMode: DesktopCredentialsMode) {
assert(SUPPORTED_MODES.includes(encryptionMode), `should not use unsupported encryption mode ${encryptionMode}`)

View file

@ -17,8 +17,8 @@ import { getFileBaseName, getFileExtension, isTutanotaFile } from "../api/common
import { getSafeAreaInsetBottom } from "./HtmlUtils.js"
import { hasError } from "../api/common/utils/ErrorUtils.js"
import { BubbleButton, bubbleButtonHeight, bubbleButtonPadding } from "./base/buttons/BubbleButton.js"
import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js"
import { BootIcons } from "./base/icons/BootIcons.js"
import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js"
export enum AttachmentType {
GENERIC,

View file

@ -152,7 +152,7 @@ export class PostLoginActions implements PostLoginAction {
if (!isAdminClient()) {
// If it failed during the partial login due to missing cache entries we will give it another spin here. If it didn't fail then it's just a noop
await locator.mailModel.init()
await locator.mailboxModel.init()
const calendarModel = await locator.calendarModel()
await calendarModel.init()
await this.remindActiveOutOfOfficeNotification()

View file

@ -0,0 +1,237 @@
import {
createMailAddressProperties,
createMailboxProperties,
Mail,
MailBox,
MailboxGroupRoot,
MailboxGroupRootTypeRef,
MailboxProperties,
MailboxPropertiesTypeRef,
MailBoxTypeRef,
MailFolder,
MailFolderTypeRef,
} from "../api/entities/tutanota/TypeRefs.js"
import { Group, GroupInfo, GroupInfoTypeRef, GroupMembership, GroupTypeRef } from "../api/entities/sys/TypeRefs.js"
import Stream from "mithril/stream"
import stream from "mithril/stream"
import { EventController } from "../api/main/EventController.js"
import { EntityClient } from "../api/common/EntityClient.js"
import { LoginController } from "../api/main/LoginController.js"
import { assertNotNull, lazyMemoized, neverNull, ofClass } from "@tutao/tutanota-utils"
import { FeatureType, MailSetKind, OperationType } from "../api/common/TutanotaConstants.js"
import { getEnabledMailAddressesWithUser } from "./SharedMailUtils.js"
import { PreconditionFailedError } from "../api/common/error/RestError.js"
import { EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
import m from "mithril"
import { ProgrammingError } from "../api/common/error/ProgrammingError.js"
import { FolderSystem } from "../api/common/mail/FolderSystem.js"
export type MailboxDetail = {
mailbox: MailBox
folders: FolderSystem
mailGroupInfo: GroupInfo
mailGroup: Group
mailboxGroupRoot: MailboxGroupRoot
}
export type MailboxCounters = Record<Id, Record<string, number>>
export class MailboxModel {
/** Empty stream until init() is finished, exposed mostly for map()-ing, use getMailboxDetails to get a promise */
readonly mailboxDetails: Stream<MailboxDetail[]> = stream()
private initialization: Promise<void> | null = null
/**
* Map from MailboxGroupRoot id to MailboxProperties
* A way to avoid race conditions in case we try to create mailbox properties from multiple places.
*
*/
private mailboxPropertiesPromises: Map<Id, Promise<MailboxProperties>> = new Map()
constructor(private readonly eventController: EventController, private readonly entityClient: EntityClient, private readonly logins: LoginController) {}
// only init listeners once
private readonly initListeners = lazyMemoized(() => {
this.eventController.addEntityListener((updates, eventOwnerGroupId) => this.entityEventsReceived(updates, eventOwnerGroupId))
})
init(): Promise<void> {
// if we are in the process of loading do not start another one in parallel
if (this.initialization) {
return this.initialization
}
this.initListeners()
return this._init()
}
private _init(): Promise<void> {
const mailGroupMemberships = this.logins.getUserController().getMailGroupMemberships()
const mailBoxDetailsPromises = mailGroupMemberships.map((m) => this.mailboxDetailsFromMembership(m))
this.initialization = Promise.all(mailBoxDetailsPromises).then((details) => {
this.mailboxDetails(details)
})
return this.initialization.catch((e) => {
console.warn("mailbox model initialization failed!", e)
this.initialization = null
throw e
})
}
/**
* load mailbox details from a mailgroup membership
*/
private async mailboxDetailsFromMembership(membership: GroupMembership): Promise<MailboxDetail> {
const [mailboxGroupRoot, mailGroupInfo, mailGroup] = await Promise.all([
this.entityClient.load(MailboxGroupRootTypeRef, membership.group),
this.entityClient.load(GroupInfoTypeRef, membership.groupInfo),
this.entityClient.load(GroupTypeRef, membership.group),
])
const mailbox = await this.entityClient.load(MailBoxTypeRef, mailboxGroupRoot.mailbox)
const folders = await this.loadFolders(neverNull(mailbox.folders).folders)
return {
mailbox,
folders: new FolderSystem(folders),
mailGroupInfo,
mailGroup,
mailboxGroupRoot,
}
}
/**
* Get the list of MailboxDetails that this user has access to from their memberships.
*
* Will wait for successful initialization.
*/
async getMailboxDetails(): Promise<Array<MailboxDetail>> {
// If details are there, use them
if (this.mailboxDetails()) {
return this.mailboxDetails()
} else {
// If they are not there, trigger loading again (just in case) but do not fail and wait until we actually have the details.
// This is so that the rest of the app is not in the broken state if details fail to load but is just waiting until the success.
return new Promise((resolve) => {
this.init()
const end = this.mailboxDetails.map((details) => {
resolve(details)
end.end(true)
})
})
}
}
async getMailboxDetailsForMailGroup(mailGroupId: Id): Promise<MailboxDetail> {
const mailboxDetails = await this.getMailboxDetails()
return assertNotNull(
mailboxDetails.find((md) => mailGroupId === md.mailGroup._id),
"Mailbox detail for mail group does not exist",
)
}
async getUserMailboxDetails(): Promise<MailboxDetail> {
const userMailGroupMembership = this.logins.getUserController().getUserMailGroupMembership()
const mailboxDetails = await this.getMailboxDetails()
return assertNotNull(
mailboxDetails.find((md) => md.mailGroup._id === userMailGroupMembership.group),
"Mailbox detail for user does not exist",
)
}
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>, eventOwnerGroupId: Id): Promise<void> {
for (const update of updates) {
if (isUpdateForTypeRef(GroupInfoTypeRef, update)) {
if (update.operation === OperationType.UPDATE) {
await this._init()
m.redraw
}
} else if (this.logins.getUserController().isUpdateForLoggedInUserInstance(update, eventOwnerGroupId)) {
let newMemberships = this.logins.getUserController().getMailGroupMemberships()
const mailboxDetails = await this.getMailboxDetails()
if (newMemberships.length !== mailboxDetails.length) {
await this._init()
m.redraw()
}
}
}
}
async getMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
// MailboxProperties is an encrypted instance that is created lazily. When we create it the reference is automatically written to the MailboxGroupRoot.
// Unfortunately we will only get updated new MailboxGroupRoot with the next EntityUpdate.
// To prevent parallel creation attempts we do two things:
// - we save the loading promise to avoid calling setup() twice in parallel
// - we set mailboxProperties reference manually (we could save the id elsewhere but it's easier this way)
// If we are already loading/creating, just return it to avoid races
const existingPromise = this.mailboxPropertiesPromises.get(mailboxGroupRoot._id)
if (existingPromise) {
return existingPromise
}
const promise: Promise<MailboxProperties> = this.loadOrCreateMailboxProperties(mailboxGroupRoot)
this.mailboxPropertiesPromises.set(mailboxGroupRoot._id, promise)
return promise.finally(() => this.mailboxPropertiesPromises.delete(mailboxGroupRoot._id))
}
async loadOrCreateMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
if (!mailboxGroupRoot.mailboxProperties) {
mailboxGroupRoot.mailboxProperties = await this.entityClient
.setup(
null,
createMailboxProperties({
_ownerGroup: mailboxGroupRoot._ownerGroup ?? "",
reportMovedMails: "0",
mailAddressProperties: [],
}),
)
.catch(
ofClass(PreconditionFailedError, (e) => {
// We try to prevent race conditions but they can still happen with multiple clients trying ot create mailboxProperties at the same time.
// We send special precondition from the server with an existing id.
if (e.data && e.data.startsWith("exists:")) {
const existingId = e.data.substring("exists:".length)
console.log("mailboxProperties already exists", existingId)
return existingId
} else {
throw new ProgrammingError(`Could not create mailboxProperties, precondition: ${e.data}`)
}
}),
)
}
const mailboxProperties = await this.entityClient.load(MailboxPropertiesTypeRef, mailboxGroupRoot.mailboxProperties)
if (mailboxProperties.mailAddressProperties.length === 0) {
await this.migrateFromOldSenderName(mailboxGroupRoot, mailboxProperties)
}
return mailboxProperties
}
/** If there was no sender name configured before take the user's name and assign it to all email addresses. */
private async migrateFromOldSenderName(mailboxGroupRoot: MailboxGroupRoot, mailboxProperties: MailboxProperties) {
const userGroupInfo = this.logins.getUserController().userGroupInfo
const legacySenderName = userGroupInfo.name
const mailboxDetails = await this.getMailboxDetailsForMailGroup(mailboxGroupRoot._id)
const mailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userGroupInfo)
for (const mailAddress of mailAddresses) {
mailboxProperties.mailAddressProperties.push(
createMailAddressProperties({
mailAddress,
senderName: legacySenderName,
}),
)
}
await this.entityClient.update(mailboxProperties)
}
loadFolders(folderListId: Id): Promise<MailFolder[]> {
return this.entityClient.loadAll(MailFolderTypeRef, folderListId).then((folders) => {
return folders.filter((f) => {
// We do not show spam or archive for external users
if (!this.logins.isInternalUserLoggedIn() && (f.folderType === MailSetKind.SPAM || f.folderType === MailSetKind.ARCHIVE)) {
return false
} else {
return !(this.logins.isEnabled(FeatureType.InternalCommunication) && f.folderType === MailSetKind.SPAM)
}
})
})
}
}

View file

@ -67,7 +67,7 @@ import { MailBodyTooLargeError } from "../api/common/error/MailBodyTooLargeError
import { createApprovalMail } from "../api/entities/monitor/TypeRefs.js"
import { CustomerPropertiesTypeRef } from "../api/entities/sys/TypeRefs.js"
import { isMailAddress } from "../misc/FormatValidator.js"
import { MailboxDetail, MailModel } from "./MailModel.js"
import { MailboxDetail, MailboxModel } from "./MailboxModel.js"
import { ContactModel } from "../contactsFunctionality/ContactModel.js"
import { getContactDisplayName } from "../contactsFunctionality/ContactUtils.js"
import { getMailBodyText } from "../api/common/CommonMailUtils.js"
@ -152,13 +152,14 @@ export class SendMailModel {
public readonly mailFacade: MailFacade,
public readonly entity: EntityClient,
public readonly logins: LoginController,
public readonly mailModel: MailModel,
public readonly mailboxModel: MailboxModel,
public readonly contactModel: ContactModel,
private readonly eventController: EventController,
public readonly mailboxDetails: MailboxDetail,
private readonly recipientsModel: RecipientsModel,
private readonly dateProvider: DateProvider,
private mailboxProperties: MailboxProperties,
private readonly needNewDraft: (mail: Mail) => Promise<boolean>,
) {
const userProps = logins.getUserController().props
this.senderAddress = this.getDefaultSender()
@ -885,7 +886,7 @@ export class SendMailModel {
}).html
this.draft =
this.draft == null || (await this.isMailInTrashOrSpam(this.draft))
this.draft == null || (await this.needNewDraft(this.draft))
? await this.createDraft(body, attachments, mailMethod)
: await this.updateDraft(body, attachments, this.draft)
@ -915,12 +916,6 @@ export class SendMailModel {
}
}
private async isMailInTrashOrSpam(draft: Mail): Promise<boolean> {
const folders = await this.mailModel.getMailboxFolders(draft)
const mailFolder = folders?.getFolderByMail(draft)
return !!mailFolder && (mailFolder.folderType === MailSetKind.TRASH || mailFolder.folderType === MailSetKind.SPAM)
}
private sendApprovalMail(body: string): Promise<unknown> {
const listId = "---------c--"
const m = createApprovalMail({

View file

@ -14,7 +14,7 @@ import {
TutanotaProperties,
} from "../api/entities/tutanota/TypeRefs.js"
import { fullNameToFirstAndLastName, mailAddressToFirstAndLastName } from "../misc/parsing/MailAddressParser.js"
import { assertNotNull, contains, endsWith, first, isNotEmpty, neverNull } from "@tutao/tutanota-utils"
import { assertNotNull, contains, neverNull } from "@tutao/tutanota-utils"
import {
ContactAddressType,
ConversationType,
@ -33,17 +33,17 @@ import { getEnabledMailAddressesForGroupInfo, getGroupInfoDisplayName } from "..
import { lang, Language, TranslationKey } from "../misc/LanguageViewModel.js"
import { AllIcons } from "../gui/base/Icon.js"
import { Icons } from "../gui/base/icons/Icons.js"
import { MailboxDetail, MailModel } from "./MailModel.js"
import { MailboxDetail } from "./MailboxModel.js"
import { LoginController } from "../api/main/LoginController.js"
import { EntityClient } from "../api/common/EntityClient.js"
import { getListId, isSameId } from "../api/common/utils/EntityUtils.js"
import type { FolderSystem, IndentedFolder } from "../api/common/mail/FolderSystem.js"
import type { FolderSystem } from "../api/common/mail/FolderSystem.js"
import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js"
import { ListFilter } from "../misc/ListModel.js"
import { FontIcons } from "../gui/base/icons/FontIcons.js"
import { ProgrammingError } from "../api/common/error/ProgrammingError.js"
import { Attachment } from "./SendMailModel.js"
import { getDisplayedSender, isDraft, isSubfolderOfType } from "../api/common/CommonMailUtils.js"
import { getDisplayedSender } from "../api/common/CommonMailUtils.js"
import { isDraft } from "../../mail-app/mail/model/MailModel.js"
assertMainOrNode()
export const LINE_BREAK = "<br>"
@ -359,26 +359,6 @@ export enum RecipientField {
export type FolderInfo = { level: number; folder: MailFolder }
export async function getMoveTargetFolderSystems(model: MailModel, mails: readonly Mail[]): Promise<Array<FolderInfo>> {
const firstMail = first(mails)
if (firstMail == null) return []
const mailboxDetails = await model.getMailboxDetailsForMail(firstMail)
if (mailboxDetails == null) {
return []
}
const folderSystem = mailboxDetails.folders
return folderSystem.getIndentedList().filter((f: IndentedFolder) => {
if (f.folder.isMailSet && isNotEmpty(firstMail.sets)) {
const folderId = firstMail.sets[0]
return !isSameId(f.folder._id, folderId)
} else {
return f.folder.mails !== getListId(firstMail)
}
})
}
export const MAX_FOLDER_INDENT_LEVEL = 10
export function getIndentedFolderNameForDropdown(folderInfo: FolderInfo) {
@ -461,29 +441,7 @@ export function isTutanotaMailAddress(mailAddress: string): boolean {
return TUTANOTA_MAIL_ADDRESS_DOMAINS.some((tutaDomain) => mailAddress.endsWith("@" + tutaDomain))
}
/**
* Gets a system folder of the specified type and unwraps it.
* Some system folders don't exist in some cases, e.g. spam or archive for external mailboxes!
*
* Use with caution.
*/
export function assertSystemFolderOfType(system: FolderSystem, type: Omit<MailSetKind, MailSetKind.CUSTOM>): MailFolder {
return assertNotNull(system.getSystemFolderByType(type), "System folder of type does not exist!")
}
export function isOfTypeOrSubfolderOf(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
return folder.folderType === type || isSubfolderOfType(system, folder, type)
}
/**
* NOTE: DOES NOT VERIFY IF THE MESSAGE IS AUTHENTIC - DO NOT USE THIS OUTSIDE OF THIS FILE OR FOR TESTING
* @VisibleForTesting
*/
export function isTutanotaTeamAddress(address: string): boolean {
return endsWith(address, "@tutao.de") || address === "no-reply@tutanota.de"
}
function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mail): boolean {
export function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mail): boolean {
switch (encryptionAuthStatus) {
// emails before tuta-crypt had no encryptionAuthStatus
case null:
@ -500,19 +458,6 @@ function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mai
}
}
/**
* Is this a tutao team member email or a system notification
*/
export function isTutanotaTeamMail(mail: Mail): boolean {
const { confidential, sender, state } = mail
return (
confidential &&
state === MailState.RECEIVED &&
hasValidEncryptionAuthForTeamOrSystemMail(mail) &&
(sender.address === SYSTEM_GROUP_MAIL_ADDRESS || isTutanotaTeamAddress(sender.address))
)
}
/**
* Is this a system notification?
*/

View file

@ -1,4 +1,4 @@
import type { MailboxDetail } from "../../mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../mailFunctionality/MailboxModel.js"
import type { LoginController } from "../../api/main/LoginController"
import { assertMainOrNode } from "../../api/common/Env"
import { PartialRecipient } from "../../api/common/recipients/Recipient"

View file

@ -1,15 +1,17 @@
import m from "mithril"
import { locator } from "../../api/main/CommonLocator"
import { MailSetKind } from "../../api/common/TutanotaConstants.js"
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js"
import { assertSystemFolderOfType } from "../../../mail-app/mail/model/MailModel.js"
import { mailLocator } from "../../../mail-app/mailLocator.js"
import { assertNotNull } from "@tutao/tutanota-utils"
import { getElementId } from "../../api/common/utils/EntityUtils.js"
export async function openMailbox(userId: Id, mailAddress: string, requestedPath: string | null) {
if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) {
if (!requestedPath) {
const [mailboxDetail] = await locator.mailModel.getMailboxDetails()
const inbox = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX)
const [mailboxDetail] = await locator.mailboxModel.getMailboxDetails()
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
const inbox = assertSystemFolderOfType(folders, MailSetKind.INBOX)
m.route.set("/mail/" + getElementId(inbox))
} else {
m.route.set("/mail" + requestedPath)

View file

@ -8,7 +8,7 @@ import { Dialog } from "../../gui/base/Dialog.js"
import { AttachmentType, getAttachmentType } from "../../gui/AttachmentBubble.js"
import { showRequestPasswordDialog } from "../../misc/passwords/PasswordRequestDialog.js"
import { LoginController } from "../../api/main/LoginController.js"
import { MailModel } from "../../mailFunctionality/MailModel.js"
import { MailboxModel } from "../../mailFunctionality/MailboxModel.js"
import { UsageTestController } from "@tutao/tutanota-usagetests"
import { NativeFileApp } from "../common/FileApp.js"
import { NativePushServiceApp } from "./NativePushServiceApp.js"
@ -18,11 +18,13 @@ import { AppType } from "../../misc/ClientConstants.js"
export class WebCommonNativeFacade implements CommonNativeFacade {
constructor(
private readonly logins: LoginController,
private readonly mailModel: MailModel,
private readonly mailboxModel: MailboxModel,
private readonly usageTestController: UsageTestController,
private readonly fileApp: lazyAsync<NativeFileApp>,
private readonly pushService: lazyAsync<NativePushServiceApp>,
private readonly fileImportHandler: (filesUris: ReadonlyArray<string>) => unknown,
readonly openMailBox: (userId: string, address: string, requestedPath: string | null) => Promise<void>,
readonly openCalendar: (userId: string) => Promise<void>,
private readonly appType: AppType,
) {}
@ -46,7 +48,7 @@ export class WebCommonNativeFacade implements CommonNativeFacade {
const { newMailEditorFromTemplate, newMailtoUrlMailEditor } = await import("../../../mail-app/mail/editor/MailEditor.js")
const signatureModule = await import("../../../mail-app/mail/signature/Signature")
await this.logins.waitForPartialLogin()
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
let editor
try {
@ -130,16 +132,6 @@ export class WebCommonNativeFacade implements CommonNativeFacade {
await pushService.reRegister()
}
async openCalendar(userId: string): Promise<void> {
const { openCalendar } = await import("./OpenMailboxHandler.js")
return openCalendar(userId)
}
async openMailBox(userId: string, address: string, requestedPath: string | null): Promise<void> {
const { openMailbox } = await import("./OpenMailboxHandler.js")
return openMailbox(userId, address, requestedPath)
}
async showAlertDialog(translationKey: string): Promise<void> {
const { Dialog } = await import("../../gui/base/Dialog.js")
return Dialog.message(translationKey as TranslationKey)

View file

@ -7,12 +7,11 @@ import { CloseEventBusOption, MailSetKind, SECOND_MS } from "../../api/common/Tu
import { MobileFacade } from "../common/generatedipc/MobileFacade.js"
import { styles } from "../../gui/styles"
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js"
import { MailModel } from "../../mailFunctionality/MailModel.js"
import { MailboxModel } from "../../mailFunctionality/MailboxModel.js"
import { TopLevelView } from "../../../TopLevelView.js"
import stream from "mithril/stream"
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js"
import { assertSystemFolderOfType } from "../../../mail-app/mail/model/MailModel.js"
import { CalendarViewType } from "../../api/common/utils/CommonCalendarUtils.js"
import { getElementId } from "../../api/common/utils/EntityUtils.js"
assertMainOrNode()
@ -27,8 +26,9 @@ export class WebMobileFacade implements MobileFacade {
constructor(
private readonly connectivityModel: WebsocketConnectivityModel,
private readonly mailModel: MailModel,
private readonly mailboxModel: MailboxModel,
private readonly baseViewPrefix: string,
private readonly mailBackNewRoute?: (currentRoute: string) => Promise<string | null>,
) {}
public getIsAppVisible(): stream<boolean> {
@ -86,28 +86,13 @@ export class WebMobileFacade implements MobileFacade {
m.route.set(this.baseViewPrefix)
return true
} else if (viewSlider && viewSlider.isFirstBackgroundColumnFocused()) {
// If the first background column is focused in mail view (showing a folder), move to inbox.
// If in inbox already, quit
if (m.route.get().startsWith(MAIL_PREFIX)) {
const parts = m.route
.get()
.split("/")
.filter((part) => part !== "")
if (parts.length > 1) {
const selectedMailFolderId = parts[1]
const [mailboxDetail] = await this.mailModel.getMailboxDetails()
const inboxMailFolderId = getElementId(assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX))
if (inboxMailFolderId !== selectedMailFolderId) {
m.route.set(MAIL_PREFIX + "/" + inboxMailFolderId)
return true
} else {
return false
}
if (currentRoute.startsWith(MAIL_PREFIX) && this.mailBackNewRoute) {
const newRoute = await this.mailBackNewRoute(currentRoute)
if (newRoute) {
m.route.set(newRoute)
return true
}
}
return false
} else {
return false

View file

@ -73,7 +73,7 @@ export class AboutDialog implements Component<AboutDialogAttrs> {
async _sendDeviceLogs(): Promise<void> {
const timestamp = new Date()
const attachments = await getLogAttachments(timestamp)
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
let { message, type, client } = clientInfoString(timestamp, true)
message = message
.split("\n")

View file

@ -8,7 +8,7 @@ import { PartialRecipient, Recipients } from "../api/common/recipients/Recipient
import { getDefaultSender, getEnabledMailAddressesWithUser, getMailAddressDisplayText, getSenderNameForUser } from "../mailFunctionality/SharedMailUtils.js"
export function sendShareNotificationEmail(sharedGroupInfo: GroupInfo, recipients: Array<PartialRecipient>, texts: GroupSharingTexts) {
locator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
locator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
const senderMailAddress = getDefaultSender(locator.logins, mailboxDetails)
const userName = getSenderNameForUser(mailboxDetails, locator.logins.getUserController())
// Sending notifications as bcc so that invited people don't see each other
@ -88,7 +88,7 @@ function _sendNotificationEmail(recipients: Recipients, subject: string, body: s
allowRelativeLinks: false,
usePlaceholderForInlineImages: false,
}).html
locator.mailModel.getUserMailboxDetails().then(async (mailboxDetails) => {
locator.mailboxModel.getUserMailboxDetails().then(async (mailboxDetails) => {
const sender = getEnabledMailAddressesWithUser(mailboxDetails, locator.logins.getUserController().userGroupInfo).includes(senderMailAddress)
? senderMailAddress
: getDefaultSender(locator.logins, mailboxDetails)
@ -96,7 +96,7 @@ function _sendNotificationEmail(recipients: Recipients, subject: string, body: s
const confirm = () => Promise.resolve(true)
const wait = showProgressDialog
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await locator.sendMailModel(mailboxDetails, mailboxProperties)
await model.initWithTemplate(recipients, subject, sanitizedBody, [], true, sender)
await model.send(MailMethod.NONE, confirm, wait, "tooManyMailsAuto_msg")

View file

@ -930,7 +930,7 @@ export class ContactView extends BaseTopLevelView implements TopLevelView<Contac
}
export function writeMail(to: PartialRecipient, subject: string = ""): Promise<unknown> {
return locator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
return locator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
return newMailEditorFromTemplate(
mailboxDetails,
{

View file

@ -5,7 +5,7 @@ import { Editor, ImagePasteEvent } from "../../../common/gui/editor/Editor"
import type { Attachment, InitAsResponseArgs, SendMailModel } from "../../../common/mailFunctionality/SendMailModel.js"
import { Dialog } from "../../../common/gui/base/Dialog"
import { InfoLink, lang } from "../../../common/misc/LanguageViewModel"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
import { checkApprovalStatus } from "../../../common/misc/LoginUtils"
import { locator } from "../../../common/api/main/CommonLocator"
import {
@ -1156,7 +1156,7 @@ export async function newMailEditorFromTemplate(
senderMailAddress?: string,
initialChangedState?: boolean,
): Promise<Dialog> {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return locator
.sendMailModel(mailboxDetails, mailboxProperties)
.then((model) => model.initWithTemplate(recipients, subject, bodyText, attachments, confidential, senderMailAddress, initialChangedState))
@ -1261,7 +1261,7 @@ export async function writeGiftCardMail(link: string, svg: SVGElement, mailboxDe
async function getMailboxDetailsAndProperties(
mailboxDetails: MailboxDetail | null | undefined,
): Promise<{ mailboxDetails: MailboxDetail; mailboxProperties: MailboxProperties }> {
mailboxDetails = mailboxDetails ?? (await locator.mailModel.getUserMailboxDetails())
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
mailboxDetails = mailboxDetails ?? (await locator.mailboxModel.getUserMailboxDetails())
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return { mailboxDetails, mailboxProperties }
}

View file

@ -4,7 +4,7 @@ import { InboxRuleType, MailSetKind, MAX_NBR_MOVE_DELETE_MAIL_SERVICE } from "..
import { isDomainName, isRegularExpression } from "../../../common/misc/FormatValidator"
import { assertNotNull, asyncFind, debounce, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
import { lang } from "../../../common/misc/LanguageViewModel"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
import type { SelectorItemList } from "../../../common/gui/base/DropDownSelector.js"
import { elementIdPart, isSameId } from "../../../common/api/common/utils/EntityUtils"
@ -13,6 +13,8 @@ import { MailFacade } from "../../../common/api/worker/facades/lazy/MailFacade.j
import { LoginController } from "../../../common/api/main/LoginController.js"
import { getMailHeaders } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { throttle } from "@tutao/tutanota-utils/dist/Utils.js"
import { assertSystemFolderOfType } from "./MailModel.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode()
const moveMailDataPerFolder: MoveMailData[] = []
@ -99,14 +101,21 @@ export class InboxRuleHandler {
* @returns true if a rule matches otherwise false
*/
async findAndApplyMatchingRule(mailboxDetail: MailboxDetail, mail: Mail, applyRulesOnServer: boolean): Promise<{ folder: MailFolder; mail: Mail } | null> {
if (mail._errors || !mail.unread || !isInboxFolder(mailboxDetail, mail) || !this.logins.getUserController().isPremiumAccount()) {
if (
mail._errors ||
!mail.unread ||
!isInboxFolder(mailboxDetail, mail) ||
!this.logins.getUserController().isPremiumAccount() ||
mailboxDetail.mailbox.folders == null
) {
return null
}
const inboxRule = await _findMatchingRule(this.mailFacade, mail, this.logins.getUserController().props.inboxRules)
if (inboxRule) {
let inboxFolder = assertNotNull(mailboxDetail.folders.getSystemFolderByType(MailSetKind.INBOX))
let targetFolder = mailboxDetail.folders.getFolderById(elementIdPart(inboxRule.targetFolder))
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
let inboxFolder = assertNotNull(folders.getSystemFolderByType(MailSetKind.INBOX))
let targetFolder = folders.getFolderById(elementIdPart(inboxRule.targetFolder))
if (targetFolder && targetFolder.folderType !== MailSetKind.INBOX) {
if (applyRulesOnServer) {
@ -225,6 +234,7 @@ function _checkEmailAddresses(mailAddresses: string[], inboxRule: InboxRule): bo
}
export function isInboxFolder(mailboxDetail: MailboxDetail, mail: Mail): boolean {
const mailFolder = mailboxDetail.folders.getFolderByMail(mail)
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
const mailFolder = folders.getFolderByMail(mail)
return mailFolder?.folderType === MailSetKind.INBOX
}

View file

@ -1,30 +1,17 @@
import Stream from "mithril/stream"
import stream from "mithril/stream"
import { MailboxCounters, MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { FolderSystem, type IndentedFolder } from "../../../common/api/common/mail/FolderSystem.js"
import { assertNotNull, first, groupBy, isNotEmpty, lazyMemoized, neverNull, noOp, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
import {
createMailAddressProperties,
createMailboxProperties,
Mail,
MailBox,
MailboxGroupRoot,
MailboxGroupRootTypeRef,
MailboxProperties,
MailboxPropertiesTypeRef,
MailBoxTypeRef,
MailFolder,
MailFolderTypeRef,
MailSetEntryTypeRef,
MailTypeRef,
} from "../api/entities/tutanota/TypeRefs.js"
import { Group, GroupInfo, GroupInfoTypeRef, GroupMembership, GroupTypeRef, WebsocketCounterData } from "../api/entities/sys/TypeRefs.js"
import { FolderSystem } from "../api/common/mail/FolderSystem.js"
import Stream from "mithril/stream"
import stream from "mithril/stream"
import { Notifications, NotificationType } from "../gui/Notifications.js"
import { EventController } from "../api/main/EventController.js"
import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js"
import { EntityClient } from "../api/common/EntityClient.js"
import { LoginController } from "../api/main/LoginController.js"
import { WebsocketConnectivityModel } from "../misc/WebsocketConnectivityModel.js"
import { InboxRuleHandler } from "../../mail-app/mail/model/InboxRuleHandler.js"
import { assertNotNull, groupBy, isNotEmpty, lazyMemoized, neverNull, noOp, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
} from "../../../common/api/entities/tutanota/TypeRefs.js"
import {
FeatureType,
MailReportType,
@ -32,138 +19,120 @@ import {
MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
OperationType,
ReportMovedMailsType,
} from "../api/common/TutanotaConstants.js"
import { assertSystemFolderOfType, getEnabledMailAddressesWithUser } from "./SharedMailUtils.js"
import { LockedError, NotFoundError, PreconditionFailedError } from "../api/common/error/RestError.js"
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../api/common/utils/EntityUtils.js"
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
} from "../../../common/api/common/TutanotaConstants.js"
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../../../common/api/common/utils/EntityUtils.js"
import { FolderInfo } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import m from "mithril"
import { lang } from "../misc/LanguageViewModel.js"
import { ProgrammingError } from "../api/common/error/ProgrammingError.js"
import { UserError } from "../api/main/UserError.js"
import { isSpamOrTrashFolder } from "../api/common/CommonMailUtils.js"
export type MailboxDetail = {
mailbox: MailBox
folders: FolderSystem
mailGroupInfo: GroupInfo
mailGroup: Group
mailboxGroupRoot: MailboxGroupRoot
}
export type MailboxCounters = Record<Id, Record<string, number>>
import { WebsocketCounterData } from "../../../common/api/entities/sys/TypeRefs.js"
import { Notifications, NotificationType } from "../../../common/gui/Notifications.js"
import { lang } from "../../../common/misc/LanguageViewModel.js"
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
import { LockedError, NotFoundError, PreconditionFailedError } from "../../../common/api/common/error/RestError.js"
import { UserError } from "../../../common/api/main/UserError.js"
import { EventController } from "../../../common/api/main/EventController.js"
import { InboxRuleHandler } from "./InboxRuleHandler.js"
import { WebsocketConnectivityModel } from "../../../common/misc/WebsocketConnectivityModel.js"
import { EntityClient } from "../../../common/api/common/EntityClient.js"
import { LoginController } from "../../../common/api/main/LoginController.js"
import { MailFacade } from "../../../common/api/worker/facades/lazy/MailFacade.js"
import { mailLocator } from "../../mailLocator.js"
export class MailModel {
/** Empty stream until init() is finished, exposed mostly for map()-ing, use getMailboxDetails to get a promise */
readonly mailboxDetails: Stream<MailboxDetail[]> = stream()
readonly mailboxCounters: Stream<MailboxCounters> = stream({})
private initialization: Promise<void> | null = null
/**
* Map from MailboxGroupRoot id to MailboxProperties
* A way to avoid race conditions in case we try to create mailbox properties from multiple places.
*
*/
private mailboxPropertiesPromises: Map<Id, Promise<MailboxProperties>> = new Map()
readonly folders: Stream<Record<Id, FolderSystem>> = stream()
constructor(
private readonly notifications: Notifications,
private readonly mailboxModel: MailboxModel,
private readonly eventController: EventController,
private readonly mailFacade: MailFacade,
private readonly entityClient: EntityClient,
private readonly logins: LoginController,
private readonly mailFacade: MailFacade,
private readonly connectivityModel: WebsocketConnectivityModel | null,
private readonly inboxRuleHandler: InboxRuleHandler | null,
) {}
// only init listeners once
private readonly initListeners = lazyMemoized(() => {
this.eventController.addEntityListener((updates, eventOwnerGroupId) => this.entityEventsReceived(updates, eventOwnerGroupId))
this.eventController.addEntityListener((updates) => this.entityEventsReceived(updates))
this.eventController.getCountersStream().map((update) => {
this._mailboxCountersUpdates(update)
})
})
init(): Promise<void> {
// if we are in the process of loading do not start another one in parallel
if (this.initialization) {
return this.initialization
}
async init(): Promise<void> {
this.initListeners()
return this._init()
}
const mailboxDetails = this.mailboxModel.mailboxDetails() || []
private _init(): Promise<void> {
const mailGroupMemberships = this.logins.getUserController().getMailGroupMemberships()
const mailBoxDetailsPromises = mailGroupMemberships.map((m) => this.mailboxDetailsFromMembership(m))
this.initialization = Promise.all(mailBoxDetailsPromises).then((details) => {
this.mailboxDetails(details)
})
return this.initialization.catch((e) => {
console.warn("mail model initialization failed!", e)
this.initialization = null
throw e
})
}
let tempFolders: Record<Id, FolderSystem> = {}
/**
* load mailbox details from a mailgroup membership
*/
private async mailboxDetailsFromMembership(membership: GroupMembership): Promise<MailboxDetail> {
const [mailboxGroupRoot, mailGroupInfo, mailGroup] = await Promise.all([
this.entityClient.load(MailboxGroupRootTypeRef, membership.group),
this.entityClient.load(GroupInfoTypeRef, membership.groupInfo),
this.entityClient.load(GroupTypeRef, membership.group),
])
const mailbox = await this.entityClient.load(MailBoxTypeRef, mailboxGroupRoot.mailbox)
const folders = await this.loadFolders(neverNull(mailbox.folders).folders)
return {
mailbox,
folders: new FolderSystem(folders),
mailGroupInfo,
mailGroup,
mailboxGroupRoot,
for (let detail of mailboxDetails) {
if (detail.mailbox.folders) {
const detailFolders = await this.mailboxModel.loadFolders(neverNull(detail.mailbox.folders).folders)
tempFolders[detail.mailbox.folders._id] = new FolderSystem(detailFolders)
}
}
this.folders(tempFolders)
}
private loadFolders(folderListId: Id): Promise<MailFolder[]> {
return this.entityClient.loadAll(MailFolderTypeRef, folderListId).then((folders) => {
return folders.filter((f) => {
// We do not show spam or archive for external users
if (!this.logins.isInternalUserLoggedIn() && (f.folderType === MailSetKind.SPAM || f.folderType === MailSetKind.ARCHIVE)) {
return false
} else {
return !(this.logins.isEnabled(FeatureType.InternalCommunication) && f.folderType === MailSetKind.SPAM)
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>): Promise<void> {
for (const update of updates) {
if (isUpdateForTypeRef(MailFolderTypeRef, update)) {
await this.init()
m.redraw()
} else if (
isUpdateForTypeRef(MailTypeRef, update) &&
update.operation === OperationType.CREATE &&
!containsEventOfType(updates, OperationType.DELETE, update.instanceId)
) {
if (this.inboxRuleHandler && this.connectivityModel) {
const mailId: IdTuple = [update.instanceListId, update.instanceId]
try {
const mail = await this.entityClient.load(MailTypeRef, mailId)
const folder = this.getMailFolderForMail(mail)
if (folder && folder.folderType === MailSetKind.INBOX) {
// If we don't find another delete operation on this email in the batch, then it should be a create operation,
// otherwise it's a move
await this.getMailboxDetailsForMail(mail)
.then((mailboxDetail) => {
// We only apply rules on server if we are the leader in case of incoming messages
return (
mailboxDetail &&
this.inboxRuleHandler?.findAndApplyMatchingRule(
mailboxDetail,
mail,
this.connectivityModel ? this.connectivityModel.isLeader() : false,
)
)
})
.then((newFolderAndMail) => {
if (newFolderAndMail) {
this._showNotification(newFolderAndMail.folder, newFolderAndMail.mail)
} else {
this._showNotification(folder, mail)
}
})
.catch(noOp)
}
} catch (e) {
if (e instanceof NotFoundError) {
console.log(`Could not find updated mail ${JSON.stringify(mailId)}`)
} else {
throw e
}
}
}
})
})
}
/**
* Get the list of MailboxDetails that this user has access to from their memberships.
*
* Will wait for successful initialization.
*/
async getMailboxDetails(): Promise<Array<MailboxDetail>> {
// If details are there, use them
if (this.mailboxDetails()) {
return this.mailboxDetails()
} else {
// If they are not there, trigger loading again (just in case) but do not fail and wait until we actually have the details.
// This is so that the rest of the app is not in the broken state if details fail to load but is just waiting until the success.
return new Promise((resolve) => {
this.init()
const end = this.mailboxDetails.map((details) => {
resolve(details)
end.end(true)
})
})
}
}
}
async getMailboxDetailsForMail(mail: Mail): Promise<MailboxDetail | null> {
const mailboxDetails = await this.getMailboxDetails()
const mailboxDetails = await this.mailboxModel.getMailboxDetails()
const detail = mailboxDetails.find((md) => md.folders.getFolderByMail(mail)) ?? null
if (detail == null) {
console.warn("Mailbox detail for mail does not exist", mail)
@ -172,7 +141,7 @@ export class MailModel {
}
async getMailboxDetailsForMailFolder(mailFolder: MailFolder): Promise<MailboxDetail | null> {
const mailboxDetails = await this.getMailboxDetails()
const mailboxDetails = await this.mailboxModel.getMailboxDetails()
const detail = mailboxDetails.find((md) => md.folders.getFolderById(getElementId(mailFolder))) ?? null
if (detail == null) {
console.warn("Mailbox detail for mail folder does not exist", mailFolder)
@ -180,29 +149,23 @@ export class MailModel {
return detail
}
async getMailboxDetailsForMailGroup(mailGroupId: Id): Promise<MailboxDetail> {
const mailboxDetails = await this.getMailboxDetails()
return assertNotNull(
mailboxDetails.find((md) => mailGroupId === md.mailGroup._id),
"Mailbox detail for mail group does not exist",
)
getMailboxFoldersForMail(mail: Mail): Promise<FolderSystem | null> {
return this.getMailboxDetailsForMail(mail).then((md) => {
if (md && md.mailbox.folders) {
const folderStructures = this.folders()
return folderStructures[md.mailbox.folders._id] ?? null
}
return null
})
}
async getUserMailboxDetails(): Promise<MailboxDetail> {
const userMailGroupMembership = this.logins.getUserController().getUserMailGroupMembership()
const mailboxDetails = await this.getMailboxDetails()
return assertNotNull(
mailboxDetails.find((md) => md.mailGroup._id === userMailGroupMembership.group),
"Mailbox detail for user does not exist",
)
}
async getMailboxFolders(mail: Mail): Promise<FolderSystem | null> {
return this.getMailboxDetailsForMail(mail).then((md) => md && md.folders)
getMailboxFoldersForId(foldersId: Id): FolderSystem {
const folderStructures = this.folders()
return folderStructures[foldersId]
}
getMailFolderForMail(mail: Mail): MailFolder | null {
const mailboxDetails = this.mailboxDetails() || []
const mailboxDetails = this.mailboxModel.mailboxDetails() || []
let foundFolder: MailFolder | null = null
for (let detail of mailboxDetails) {
@ -217,27 +180,6 @@ export class MailModel {
return null
}
/**
* Sends the given folder and all its descendants to the spam folder, reporting mails (if applicable) and removes any empty folders
*/
async sendFolderToSpam(folder: MailFolder): Promise<void> {
const mailboxDetail = await this.getMailboxDetailsForMailFolder(folder)
if (mailboxDetail == null) {
return
}
let deletedFolder = await this.removeAllEmpty(mailboxDetail, folder)
if (!deletedFolder) {
return this.mailFacade.updateMailFolderParent(folder, assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)._id)
}
}
async reportMails(reportType: MailReportType, mails: ReadonlyArray<Mail>): Promise<void> {
for (const mail of mails) {
await this.mailFacade.reportMail(mail, reportType).catch(ofClass(NotFoundError, (e) => console.log("mail to be reported not found", e)))
}
}
/**
* Finally move all given mails. Caller must ensure that mails are only from
* * one folder (because we send one source folder)
@ -284,27 +226,6 @@ export class MailModel {
}
}
isMovingMailsAllowed(): boolean {
return this.logins.getUserController().isInternalUser()
}
isExportingMailsAllowed(): boolean {
return !this.logins.isEnabled(FeatureType.DisableMailExport)
}
async markMails(mails: readonly Mail[], unread: boolean): Promise<void> {
await promiseMap(
mails,
async (mail) => {
if (mail.unread !== unread) {
mail.unread = unread
return this.entityClient.update(mail).catch(ofClass(NotFoundError, noOp)).catch(ofClass(LockedError, noOp))
}
},
{ concurrency: 5 },
)
}
/**
* Finally deletes the given mails if they are already in the trash or spam folders,
* otherwise moves them to the trash folder.
@ -357,71 +278,62 @@ export class MailModel {
}
}
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>, eventOwnerGroupId: Id): Promise<void> {
for (const update of updates) {
if (isUpdateForTypeRef(MailFolderTypeRef, update)) {
await this._init()
m.redraw()
} else if (isUpdateForTypeRef(GroupInfoTypeRef, update)) {
if (update.operation === OperationType.UPDATE) {
await this._init()
m.redraw
}
} else if (this.logins.getUserController().isUpdateForLoggedInUserInstance(update, eventOwnerGroupId)) {
let newMemberships = this.logins.getUserController().getMailGroupMemberships()
const mailboxDetails = await this.getMailboxDetails()
/**
* Finally deletes all given mails. Caller must ensure that mails are only from one folder and the folder must allow final delete operation.
*/
private async finallyDeleteMails(mails: Mail[]): Promise<void> {
if (!mails.length) return Promise.resolve()
const mailFolder = neverNull(this.getMailFolderForMail(mails[0]))
const mailIds = mails.map((m) => m._id)
const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, mailIds)
if (newMemberships.length !== mailboxDetails.length) {
await this._init()
m.redraw()
}
} else if (
isUpdateForTypeRef(MailTypeRef, update) &&
update.operation === OperationType.CREATE &&
!containsEventOfType(updates, OperationType.DELETE, update.instanceId)
) {
if (this.inboxRuleHandler && this.connectivityModel) {
const mailId: IdTuple = [update.instanceListId, update.instanceId]
try {
const mail = await this.entityClient.load(MailTypeRef, mailId)
const folder = this.getMailFolderForMail(mail)
if (folder && folder.folderType === MailSetKind.INBOX) {
// If we don't find another delete operation on this email in the batch, then it should be a create operation,
// otherwise it's a move
await this.getMailboxDetailsForMail(mail)
.then((mailboxDetail) => {
// We only apply rules on server if we are the leader in case of incoming messages
return (
mailboxDetail &&
this.inboxRuleHandler?.findAndApplyMatchingRule(
mailboxDetail,
mail,
this.connectivityModel ? this.connectivityModel.isLeader() : false,
)
)
})
.then((newFolderAndMail) => {
if (newFolderAndMail) {
this._showNotification(newFolderAndMail.folder, newFolderAndMail.mail)
} else {
this._showNotification(folder, mail)
}
})
.catch(noOp)
}
} catch (e) {
if (e instanceof NotFoundError) {
console.log(`Could not find updated mail ${JSON.stringify(mailId)}`)
} else {
throw e
}
}
}
}
for (const mailChunk of mailChunks) {
await this.mailFacade.deleteMails(mailChunk, mailFolder._id)
}
}
/**
* Sends the given folder and all its descendants to the spam folder, reporting mails (if applicable) and removes any empty folders
*/
async sendFolderToSpam(folder: MailFolder): Promise<void> {
const mailboxDetail = await this.getMailboxDetailsForMailFolder(folder)
if (mailboxDetail == null) {
return
}
let deletedFolder = await this.removeAllEmpty(mailboxDetail, folder)
if (!deletedFolder) {
return this.mailFacade.updateMailFolderParent(folder, assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)._id)
}
}
async reportMails(reportType: MailReportType, mails: ReadonlyArray<Mail>): Promise<void> {
for (const mail of mails) {
await this.mailFacade.reportMail(mail, reportType).catch(ofClass(NotFoundError, (e) => console.log("mail to be reported not found", e)))
}
}
isMovingMailsAllowed(): boolean {
return this.logins.getUserController().isInternalUser()
}
isExportingMailsAllowed(): boolean {
return !this.logins.isEnabled(FeatureType.DisableMailExport)
}
async markMails(mails: readonly Mail[], unread: boolean): Promise<void> {
await promiseMap(
mails,
async (mail) => {
if (mail.unread !== unread) {
mail.unread = unread
return this.entityClient.update(mail).catch(ofClass(NotFoundError, noOp)).catch(ofClass(LockedError, noOp))
}
},
{ concurrency: 5 },
)
}
_mailboxCountersUpdates(counters: WebsocketCounterData) {
const normalized = this.mailboxCounters() || {}
const group = normalized[counters.mailGroup] || {}
@ -562,77 +474,80 @@ export class MailModel {
await this.mailFacade.unsubscribe(mail._id, recipient, headers)
}
async getMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
// MailboxProperties is an encrypted instance that is created lazily. When we create it the reference is automatically written to the MailboxGroupRoot.
// Unfortunately we will only get updated new MailboxGroupRoot with the next EntityUpdate.
// To prevent parallel creation attempts we do two things:
// - we save the loading promise to avoid calling setup() twice in parallel
// - we set mailboxProperties reference manually (we could save the id elsewhere but it's easier this way)
// If we are already loading/creating, just return it to avoid races
const existingPromise = this.mailboxPropertiesPromises.get(mailboxGroupRoot._id)
if (existingPromise) {
return existingPromise
}
const promise: Promise<MailboxProperties> = this.loadOrCreateMailboxProperties(mailboxGroupRoot)
this.mailboxPropertiesPromises.set(mailboxGroupRoot._id, promise)
return promise.finally(() => this.mailboxPropertiesPromises.delete(mailboxGroupRoot._id))
}
private async loadOrCreateMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
if (!mailboxGroupRoot.mailboxProperties) {
mailboxGroupRoot.mailboxProperties = await this.entityClient
.setup(
null,
createMailboxProperties({
_ownerGroup: mailboxGroupRoot._ownerGroup ?? "",
reportMovedMails: "0",
mailAddressProperties: [],
}),
)
.catch(
ofClass(PreconditionFailedError, (e) => {
// We try to prevent race conditions but they can still happen with multiple clients trying ot create mailboxProperties at the same time.
// We send special precondition from the server with an existing id.
if (e.data && e.data.startsWith("exists:")) {
const existingId = e.data.substring("exists:".length)
console.log("mailboxProperties already exists", existingId)
return existingId
} else {
throw new ProgrammingError(`Could not create mailboxProperties, precondition: ${e.data}`)
}
}),
)
}
const mailboxProperties = await this.entityClient.load(MailboxPropertiesTypeRef, mailboxGroupRoot.mailboxProperties)
if (mailboxProperties.mailAddressProperties.length === 0) {
await this.migrateFromOldSenderName(mailboxGroupRoot, mailboxProperties)
}
return mailboxProperties
}
/** If there was no sender name configured before take the user's name and assign it to all email addresses. */
private async migrateFromOldSenderName(mailboxGroupRoot: MailboxGroupRoot, mailboxProperties: MailboxProperties) {
const userGroupInfo = this.logins.getUserController().userGroupInfo
const legacySenderName = userGroupInfo.name
const mailboxDetails = await this.getMailboxDetailsForMailGroup(mailboxGroupRoot._id)
const mailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userGroupInfo)
for (const mailAddress of mailAddresses) {
mailboxProperties.mailAddressProperties.push(
createMailAddressProperties({
mailAddress,
senderName: legacySenderName,
}),
)
}
await this.entityClient.update(mailboxProperties)
}
async saveReportMovedMails(mailboxGroupRoot: MailboxGroupRoot, reportMovedMails: ReportMovedMailsType): Promise<MailboxProperties> {
const mailboxProperties = await this.loadOrCreateMailboxProperties(mailboxGroupRoot)
const mailboxProperties = await this.mailboxModel.loadOrCreateMailboxProperties(mailboxGroupRoot)
mailboxProperties.reportMovedMails = reportMovedMails
await this.entityClient.update(mailboxProperties)
return mailboxProperties
}
async getMailboxFolders(mail: Mail): Promise<FolderSystem | null> {
return this.getMailboxDetailsForMail(mail).then((md) => md && md.folders)
}
}
export async function getMoveTargetFolderSystems(foldersModel: MailModel, mails: readonly Mail[]): Promise<Array<FolderInfo>> {
const firstMail = first(mails)
if (firstMail == null) return []
const mailboxDetails = await foldersModel.getMailboxDetailsForMail(firstMail)
if (mailboxDetails == null || mailboxDetails.mailbox.folders == null) {
return []
}
const folderStructures = foldersModel.folders()
const folderSystem = folderStructures[mailboxDetails.mailbox.folders._id]
return folderSystem.getIndentedList().filter((f: IndentedFolder) => {
if (f.folder.isMailSet && isNotEmpty(firstMail.sets)) {
const folderId = firstMail.sets[0]
return !isSameId(f.folder._id, folderId)
} else {
return f.folder.mails !== getListId(firstMail)
}
})
}
export function isSubfolderOfType(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
const systemFolder = system.getSystemFolderByType(type)
return systemFolder != null && system.checkFolderForAncestor(folder, systemFolder._id)
}
export function isDraft(mail: Mail): boolean {
return mail.mailDetailsDraft != null
}
export async function isMailInSpamOrTrash(mail: Mail): Promise<boolean> {
const folders = await mailLocator.mailModel.getMailboxFoldersForMail(mail)
const mailFolder = folders?.getFolderByMail(mail)
if (folders && mailFolder) {
return isSpamOrTrashFolder(folders, mailFolder)
} else {
return false
}
}
/**
* Returns true if given folder is the {@link MailFolderType.SPAM} or {@link MailFolderType.TRASH} folder, or a descendant of those folders.
*/
export function isSpamOrTrashFolder(system: FolderSystem, folder: MailFolder): boolean {
// not using isOfTypeOrSubfolderOf because checking the type first is cheaper
return (
folder.folderType === MailSetKind.TRASH ||
folder.folderType === MailSetKind.SPAM ||
isSubfolderOfType(system, folder, MailSetKind.TRASH) ||
isSubfolderOfType(system, folder, MailSetKind.SPAM)
)
}
/**
* Gets a system folder of the specified type and unwraps it.
* Some system folders don't exist in some cases, e.g. spam or archive for external mailboxes!
*
* Use with caution.
*/
export function assertSystemFolderOfType(system: FolderSystem, type: Omit<MailSetKind, MailSetKind.CUSTOM>): MailFolder {
return assertNotNull(system.getSystemFolderByType(type), "System folder of type does not exist!")
}
export function isOfTypeOrSubfolderOf(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
return folder.folderType === type || isSubfolderOfType(system, folder, type)
}

View file

@ -6,7 +6,7 @@ import { ButtonType } from "../../../common/gui/base/Button.js"
import { isMailAddress } from "../../../common/misc/FormatValidator"
import { UserError } from "../../../common/api/main/UserError"
import { showUserError } from "../../../common/misc/ErrorHandlerImpl"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
import { Keys, MailMethod, TabIndex } from "../../../common/api/common/TutanotaConstants"
import { progressIcon } from "../../../common/gui/base/Icon"
import { Editor } from "../../../common/gui/editor/Editor"
@ -29,7 +29,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
}
async function send() {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const body = pressRelease.bodyHtml()
const subject = pressRelease.subject()
let recipients
@ -112,7 +112,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
const bodyWithGreeting = `<p>${recipient.greeting},</p>${body}`
try {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const sendMailModel = await locator.sendMailModel(mailboxDetails, mailboxProperties)
const model = await sendMailModel.initWithTemplate(
{

View file

@ -8,10 +8,11 @@ import { LoadingStateTracker } from "../../../common/offline/LoadingState.js"
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
import { ConversationType, MailSetKind, MailState, OperationType } from "../../../common/api/common/TutanotaConstants.js"
import { NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError.js"
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.js"
import { isOfTypeOrSubfolderOf } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isOfTypeOrSubfolderOf, MailModel } from "../model/MailModel.js"
export type MailViewerViewModelFactory = (options: CreateMailViewerOptions) => MailViewerViewModel
@ -257,7 +258,11 @@ export class ConversationViewModel {
private async isInTrash(mail: Mail) {
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(mail)
const mailFolder = this.mailModel.getMailFolderForMail(mail)
return mailFolder && mailboxDetail && isOfTypeOrSubfolderOf(mailboxDetail.folders, mailFolder, MailSetKind.TRASH)
if (mailFolder == null || mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
return
}
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
return isOfTypeOrSubfolderOf(folders, mailFolder, MailSetKind.TRASH)
}
conversationItems(): ReadonlyArray<ConversationItem> {

View file

@ -6,14 +6,17 @@ import { Dialog } from "../../../common/gui/base/Dialog.js"
import { locator } from "../../../common/api/main/CommonLocator.js"
import { LockedError } from "../../../common/api/common/error/RestError.js"
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel.js"
import { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
import { elementIdPart, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js"
import { reportMailsAutomatically } from "./MailReportDialog.js"
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
import { getFolderName, getIndentedFolderNameForDropdown, getPathToFolderString } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
import { isSpamOrTrashFolder } from "../model/MailModel.js"
import { groupByAndMap } from "@tutao/tutanota-utils"
import { mailLocator } from "../../mailLocator.js"
import { assertNotNull } from "@tutao/tutanota-utils"
import type { FolderSystem, IndentedFolder } from "../../../common/api/common/mail/FolderSystem.js"
/**
* Dialog for Edit and Add folder are the same.
@ -22,12 +25,13 @@ import { groupByAndMap } from "@tutao/tutanota-utils"
export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedFolder: MailFolder | null = null, parentFolder: MailFolder | null = null) {
const noParentFolderOption = lang.get("comboBoxSelectionNone_msg")
const mailGroupId = mailBoxDetail.mailGroup._id
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailBoxDetail.mailbox.folders)._id)
let folderNameValue = editedFolder?.name ?? ""
let targetFolders: SelectorItemList<MailFolder | null> = mailBoxDetail.folders
let targetFolders: SelectorItemList<MailFolder | null> = folders
.getIndentedList(editedFolder)
// filter: SPAM and TRASH and descendants are only shown if editing (folders can only be moved there, not created there)
.filter((folderInfo) => !(editedFolder === null && isSpamOrTrashFolder(mailBoxDetail.folders, folderInfo.folder)))
.map((folderInfo) => {
.filter((folderInfo: IndentedFolder) => !(editedFolder === null && isSpamOrTrashFolder(folders, folderInfo.folder)))
.map((folderInfo: IndentedFolder) => {
return {
name: getIndentedFolderNameForDropdown(folderInfo),
value: folderInfo.folder,
@ -49,7 +53,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
selectedValue: selectedParentFolder,
selectedValueDisplay: selectedParentFolder ? getFolderName(selectedParentFolder) : noParentFolderOption,
selectionChangedHandler: (newFolder: MailFolder | null) => (selectedParentFolder = newFolder),
helpLabel: () => (selectedParentFolder ? getPathToFolderString(mailBoxDetail.folders, selectedParentFolder) : ""),
helpLabel: () => (selectedParentFolder ? getPathToFolderString(folders, selectedParentFolder) : ""),
}),
]
@ -91,7 +95,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
if (!confirmed) return
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
await locator.mailModel.trashFolderAndSubfolders(editedFolder)
await mailLocator.mailModel.trashFolderAndSubfolders(editedFolder)
} else if (selectedParentFolder?.folderType === MailSetKind.SPAM && !isSameId(selectedParentFolder._id, editedFolder.parentFolder)) {
// if it is being moved to spam (and not already in spam), ask about reporting containing emails
const confirmed = await Dialog.confirm(() =>
@ -102,16 +106,16 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
if (!confirmed) return
// get mails to report before moving to mail model
const descendants = mailBoxDetail.folders.getDescendantFoldersOfParent(editedFolder._id).sort((l, r) => r.level - l.level)
const descendants = folders.getDescendantFoldersOfParent(editedFolder._id).sort((l: IndentedFolder, r: IndentedFolder) => r.level - l.level)
let reportableMails: Array<Mail> = []
await loadAllMailsOfFolder(editedFolder, reportableMails)
for (const descendant of descendants) {
await loadAllMailsOfFolder(descendant.folder, reportableMails)
}
await reportMailsAutomatically(MailReportType.SPAM, locator.mailModel, mailBoxDetail, reportableMails)
await reportMailsAutomatically(MailReportType.SPAM, locator.mailboxModel, mailLocator.mailModel, mailBoxDetail, reportableMails)
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
await locator.mailModel.sendFolderToSpam(editedFolder)
await mailLocator.mailModel.sendFolderToSpam(editedFolder)
} else {
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
await locator.mailFacade.updateMailFolderParent(editedFolder, selectedParentFolder?._id || null)
@ -127,16 +131,22 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
Dialog.showActionDialog({
title: editedFolder ? lang.get("editFolder_action") : lang.get("addFolder_action"),
child: form,
validator: () => checkFolderName(mailBoxDetail, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null),
validator: () => checkFolderName(mailBoxDetail, folders, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null),
allowOkWithReturn: true,
okAction: okAction,
})
}
function checkFolderName(mailboxDetail: MailboxDetail, name: string, mailGroupId: Id, parentFolderId: IdTuple | null): TranslationKey | null {
function checkFolderName(
mailboxDetail: MailboxDetail,
folders: FolderSystem,
name: string,
mailGroupId: Id,
parentFolderId: IdTuple | null,
): TranslationKey | null {
if (name.trim() === "") {
return "folderNameNeutral_msg"
} else if (mailboxDetail.folders.getCustomFoldersOfParent(parentFolderId).some((f) => f.name === name)) {
} else if (folders.getCustomFoldersOfParent(parentFolderId).some((f) => f.name === name)) {
return "folderNameInvalidExisting_msg"
} else {
return null

View file

@ -100,7 +100,10 @@ export function sendResponse(event: CalendarEvent, recipient: string, status: Ca
return
}
const replyResult = await calendarInviteHandler.replyToEventInvitation(latestEvent, ownAttendee, status, previousMail)
const mailboxDetails = await mailLocator.mailModel.getMailboxDetailsForMail(previousMail)
if (mailboxDetails == null) return
const replyResult = await calendarInviteHandler.replyToEventInvitation(latestEvent, ownAttendee, status, previousMail, mailboxDetails)
if (replyResult === ReplyResult.ReplySent) {
ownAttendee.status = status
}

View file

@ -1,14 +1,14 @@
import m, { Child, Children, Component, Vnode } from "mithril"
import { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
import { locator } from "../../../common/api/main/CommonLocator.js"
import { SidebarSection } from "../../../common/gui/SidebarSection.js"
import { IconButton, IconButtonAttrs } from "../../../common/gui/base/IconButton.js"
import { FolderSubtree } from "../../../common/api/common/mail/FolderSystem.js"
import { FolderSubtree, FolderSystem } from "../../../common/api/common/mail/FolderSystem.js"
import { elementIdPart, getElementId } from "../../../common/api/common/utils/EntityUtils.js"
import { isSelectedPrefix, NavButtonAttrs, NavButtonColor } from "../../../common/gui/base/NavButton.js"
import { MAIL_PREFIX } from "../../../common/misc/RouteChange.js"
import { MailFolderRow } from "./MailFolderRow.js"
import { last, noOp, Thunk } from "@tutao/tutanota-utils"
import { assertNotNull, last, noOp, Thunk } from "@tutao/tutanota-utils"
import { MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { attachDropdown, DropdownButtonAttrs } from "../../../common/gui/base/Dropdown.js"
import { Icons } from "../../../common/gui/base/icons/Icons.js"
@ -18,9 +18,11 @@ import { MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
import { px, size } from "../../../common/gui/size.js"
import { RowButton } from "../../../common/gui/base/buttons/RowButton.js"
import { getFolderIcon, getFolderName, MAX_FOLDER_INDENT_LEVEL } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
import { isSpamOrTrashFolder, MailModel } from "../model/MailModel.js"
import { mailLocator } from "../../mailLocator.js"
export interface MailFolderViewAttrs {
mailModel: MailModel
mailboxDetail: MailboxDetail
mailFolderElementIdToSelectedMailId: ReadonlyMap<Id, Id>
onFolderClick: (folder: MailFolder) => unknown
@ -41,20 +43,21 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
private visibleRow: string | null = null
view({ attrs }: Vnode<MailFolderViewAttrs>): Children {
const { mailboxDetail } = attrs
const groupCounters = locator.mailModel.mailboxCounters()[mailboxDetail.mailGroup._id] || {}
const { mailboxDetail, mailModel } = attrs
const groupCounters = mailModel.mailboxCounters()[mailboxDetail.mailGroup._id] || {}
const folders = mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
// Important: this array is keyed so each item must have a key and `null` cannot be in the array
// So instead we push or not push into array
const customSystems = mailboxDetail.folders.customSubtrees
const systemSystems = mailboxDetail.folders.systemSubtrees
const customSystems = folders.customSubtrees
const systemSystems = folders.systemSubtrees
const children: Children = []
const selectedFolder = mailboxDetail.folders
const selectedFolder = folders
.getIndentedList()
.map((f) => f.folder)
.find((f) => isSelectedPrefix(MAIL_PREFIX + "/" + getElementId(f)))
const path = selectedFolder ? mailboxDetail.folders.getPathToFolder(selectedFolder._id) : []
const path = selectedFolder ? folders.getPathToFolder(selectedFolder._id) : []
const isInternalUser = locator.logins.isInternalUserLoggedIn()
const systemChildren = this.renderFolderTree(systemSystems, groupCounters, attrs, path, isInternalUser)
const systemChildren = this.renderFolderTree(systemSystems, groupCounters, folders, attrs, path, isInternalUser)
if (systemChildren) {
children.push(...systemChildren.children)
}
@ -67,7 +70,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
button: attrs.inEditMode ? this.renderCreateFolderAddButton(null, attrs) : this.renderEditFoldersButton(attrs),
key: "yourFolders", // we need to set a key because folder rows also have a key.
},
this.renderFolderTree(customSystems, groupCounters, attrs, path, isInternalUser).children,
this.renderFolderTree(customSystems, groupCounters, folders, attrs, path, isInternalUser).children,
),
)
children.push(this.renderAddFolderButtonRow(attrs))
@ -78,6 +81,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
private renderFolderTree(
subSystems: readonly FolderSubtree[],
groupCounters: Counters,
folders: FolderSystem,
attrs: MailFolderViewAttrs,
path: MailFolder[],
isInternalUser: boolean,
@ -115,13 +119,13 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
const summedCount = !currentExpansionState && hasChildren ? this.getTotalFolderCounter(groupCounters, system) : groupCounters[counterId]
const childResult =
hasChildren && currentExpansionState
? this.renderFolderTree(system.children, groupCounters, attrs, path, isInternalUser, indentationLevel + 1)
? this.renderFolderTree(system.children, groupCounters, folders, attrs, path, isInternalUser, indentationLevel + 1)
: { children: null, numRows: 0 }
const isTrashOrSpam = system.folder.folderType === MailSetKind.TRASH || system.folder.folderType === MailSetKind.SPAM
const isRightButtonVisible = this.visibleRow === id
const rightButton =
isInternalUser && !isTrashOrSpam && (isRightButtonVisible || attrs.inEditMode)
? this.createFolderMoreButton(system.folder, attrs, () => {
? this.createFolderMoreButton(system.folder, folders, attrs, () => {
this.visibleRow = null
})
: null
@ -177,7 +181,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
return (counters[counterId] ?? 0) + system.children.reduce((acc, child) => acc + this.getTotalFolderCounter(counters, child), 0)
}
private createFolderMoreButton(folder: MailFolder, attrs: MailFolderViewAttrs, onClose: Thunk): IconButtonAttrs {
private createFolderMoreButton(folder: MailFolder, folders: FolderSystem, attrs: MailFolderViewAttrs, onClose: Thunk): IconButtonAttrs {
return attachDropdown({
mainButtonAttrs: {
title: "more_label",
@ -188,9 +192,9 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
childAttrs: () => {
return folder.folderType === MailSetKind.CUSTOM
? // cannot add new folder to custom folder in spam or trash folder
isSpamOrTrashFolder(attrs.mailboxDetail.folders, folder)
? [this.editButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
: [this.editButtonAttrs(attrs, folder), this.addButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
isSpamOrTrashFolder(folders, folder)
? [this.editButtonAttrs(attrs, folders, folder), this.deleteButtonAttrs(attrs, folder)]
: [this.editButtonAttrs(attrs, folders, folder), this.addButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
: [this.addButtonAttrs(attrs, folder)]
},
onClose,
@ -217,7 +221,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
}
}
private editButtonAttrs(attrs: MailFolderViewAttrs, folder: MailFolder): DropdownButtonAttrs {
private editButtonAttrs(attrs: MailFolderViewAttrs, folders: FolderSystem, folder: MailFolder): DropdownButtonAttrs {
return {
label: "edit_action",
icon: Icons.Edit,
@ -225,7 +229,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
attrs.onShowFolderAddEditDialog(
attrs.mailboxDetail.mailGroup._id,
folder,
folder.parentFolder ? attrs.mailboxDetail.folders.getFolderById(elementIdPart(folder.parentFolder)) : null,
folder.parentFolder ? folders.getFolderById(elementIdPart(folder.parentFolder)) : null,
)
},
}

View file

@ -1,14 +1,14 @@
import type { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import type { File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { createMail } from "../../../common/api/entities/tutanota/TypeRefs.js"
import type { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { createMail, File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
import { Dialog } from "../../../common/gui/base/Dialog"
import { locator } from "../../../common/api/main/CommonLocator"
import { AllIcons } from "../../../common/gui/base/Icon"
import { Icons } from "../../../common/gui/base/icons/Icons"
import { isApp, isDesktop } from "../../../common/api/common/Env"
import { assertNotNull, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants"
import { assertNotNull, endsWith, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
import { MailReportType, MailSetKind, MailState, SYSTEM_GROUP_MAIL_ADDRESS } from "../../../common/api/common/TutanotaConstants"
import { getElementId } from "../../../common/api/common/utils/EntityUtils"
import { reportMailsAutomatically } from "./MailReportDialog"
import { DataFile } from "../../../common/api/common/DataFile"
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel"
@ -20,26 +20,25 @@ import { size } from "../../../common/gui/size.js"
import { PinchZoom } from "../../../common/gui/PinchZoom.js"
import { InlineImageReference, InlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
import {
assertSystemFolderOfType,
getFolderIcon,
getFolderName,
getIndentedFolderNameForDropdown,
getMoveTargetFolderSystems,
isOfTypeOrSubfolderOf,
hasValidEncryptionAuthForTeamOrSystemMail,
} from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
import { getElementId } from "../../../common/api/common/utils/EntityUtils.js"
import { assertSystemFolderOfType, getMoveTargetFolderSystems, isOfTypeOrSubfolderOf, isSpamOrTrashFolder, MailModel } from "../model/MailModel.js"
import { mailLocator } from "../../mailLocator.js"
import type { FolderSystem } from "../../../common/api/common/mail/FolderSystem.js"
export async function showDeleteConfirmationDialog(mails: ReadonlyArray<Mail>): Promise<boolean> {
let trashMails: Mail[] = []
let moveMails: Mail[] = []
for (let mail of mails) {
const folder = locator.mailModel.getMailFolderForMail(mail)
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMail(mail)
if (mailboxDetail == null) {
const folder = mailLocator.mailModel.getMailFolderForMail(mail)
const folders = await mailLocator.mailModel.getMailboxFoldersForMail(mail)
if (folders == null) {
continue
}
const isFinalDelete = folder && isSpamOrTrashFolder(mailboxDetail.folders, folder)
const isFinalDelete = folder && isSpamOrTrashFolder(folders, folder)
isFinalDelete ? trashMails.push(mail) : moveMails.push(mail)
}
@ -85,6 +84,7 @@ export function promptAndDeleteMails(mailModel: MailModel, mails: ReadonlyArray<
}
interface MoveMailsParams {
mailboxModel: MailboxModel
mailModel: MailModel
mails: ReadonlyArray<Mail>
targetMailFolder: MailFolder
@ -95,12 +95,12 @@ interface MoveMailsParams {
* Moves the mails and reports them as spam if the user or settings allow it.
* @return whether mails were actually moved
*/
export async function moveMails({ mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
export async function moveMails({ mailboxModel, mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
const details = await mailModel.getMailboxDetailsForMailFolder(targetMailFolder)
if (details == null) {
if (details == null || details.mailbox.folders == null) {
return false
}
const system = details.folders
const system = mailModel.getMailboxFoldersForId(details.mailbox.folders._id)
return mailModel
.moveMails(mails, targetMailFolder)
.then(async () => {
@ -111,8 +111,8 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
reportableMail._id = targetMailFolder.isMailSet ? mail._id : [targetMailFolder.mails, getElementId(mail)]
return reportableMail
})
const mailboxDetails = await mailModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
await reportMailsAutomatically(MailReportType.SPAM, mailModel, mailboxDetails, reportableMails)
const mailboxDetails = await mailboxModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
await reportMailsAutomatically(MailReportType.SPAM, mailboxModel, mailModel, mailboxDetails, reportableMails)
}
return true
@ -130,10 +130,11 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
export function archiveMails(mails: Mail[]): Promise<void> {
if (mails.length > 0) {
// assume all mails in the array belong to the same Mailbox
return locator.mailModel.getMailboxFolders(mails[0]).then((folders) => {
return mailLocator.mailModel.getMailboxFoldersForMail(mails[0]).then((folders: FolderSystem) => {
folders &&
moveMails({
mailModel: locator.mailModel,
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: mails,
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.ARCHIVE),
})
@ -146,10 +147,11 @@ export function archiveMails(mails: Mail[]): Promise<void> {
export function moveToInbox(mails: Mail[]): Promise<any> {
if (mails.length > 0) {
// assume all mails in the array belong to the same Mailbox
return locator.mailModel.getMailboxFolders(mails[0]).then((folders) => {
return mailLocator.mailModel.getMailboxFoldersForMail(mails[0]).then((folders: FolderSystem) => {
folders &&
moveMails({
mailModel: locator.mailModel,
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: mails,
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.INBOX),
})
@ -160,7 +162,7 @@ export function moveToInbox(mails: Mail[]): Promise<any> {
}
export function getMailFolderIcon(mail: Mail): AllIcons {
let folder = locator.mailModel.getMailFolderForMail(mail)
let folder = mailLocator.mailModel.getMailFolderForMail(mail)
if (folder) {
return getFolderIcon(folder)
@ -291,6 +293,7 @@ export function getReferencedAttachments(attachments: Array<TutanotaFile>, refer
}
export async function showMoveMailsDropdown(
mailboxModel: MailboxModel,
model: MailModel,
origin: PosRect,
mails: readonly Mail[],
@ -307,7 +310,7 @@ export async function showMoveMailsDropdown(
text: () => getIndentedFolderNameForDropdown(f),
click: () => {
onSelected()
moveMails({ mailModel: model, mails: mails, targetMailFolder: f.folder })
moveMails({ mailboxModel, mailModel: model, mails: mails, targetMailFolder: f.folder })
},
icon: getFolderIcon(f.folder),
} satisfies DropdownChildAttrs),
@ -335,3 +338,24 @@ export function getMoveMailBounds(): PosRect {
// just putting the move mail dropdown in the left side of the viewport with a bit of margin
return new DomRectReadOnlyPolyfilled(size.hpad_large, size.vpad_large, 0, 0)
}
/**
* NOTE: DOES NOT VERIFY IF THE MESSAGE IS AUTHENTIC - DO NOT USE THIS OUTSIDE OF THIS FILE OR FOR TESTING
* @VisibleForTesting
*/
export function isTutanotaTeamAddress(address: string): boolean {
return endsWith(address, "@tutao.de") || address === "no-reply@tutanota.de"
}
/**
* Is this a tutao team member email or a system notification
*/
export function isTutanotaTeamMail(mail: Mail): boolean {
const { confidential, sender, state } = mail
return (
confidential &&
state === MailState.RECEIVED &&
hasValidEncryptionAuthForTeamOrSystemMail(mail) &&
(sender.address === SYSTEM_GROUP_MAIL_ADDRESS || isTutanotaTeamAddress(sender.address))
)
}

View file

@ -29,8 +29,10 @@ import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js"
import { theme } from "../../../common/gui/theme.js"
import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
import { isKeyPressed } from "../../../common/misc/KeyManager.js"
import { assertSystemFolderOfType, canDoDragAndDropExport, isOfTypeOrSubfolderOf } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { ListModel } from "../../../common/misc/ListModel.js"
import { canDoDragAndDropExport } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { assertSystemFolderOfType, isOfTypeOrSubfolderOf } from "../model/MailModel.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode()
@ -402,14 +404,16 @@ export class MailListView implements Component<MailListViewAttrs> {
const selectedFolder = this.mailViewModel.getFolder()
if (selectedFolder) {
const mailDetails = await this.mailViewModel.getMailboxDetails()
return isOfTypeOrSubfolderOf(mailDetails.folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH
} else {
return false
if (mailDetails.mailbox.folders) {
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailDetails.mailbox.folders._id)
return isOfTypeOrSubfolderOf(folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH
}
}
return false
}
private async onSwipeLeft(listElement: Mail): Promise<ListSwipeDecision> {
const wereDeleted = await promptAndDeleteMails(locator.mailModel, [listElement], () => this.mailViewModel.listModel?.selectNone())
const wereDeleted = await promptAndDeleteMails(mailLocator.mailModel, [listElement], () => this.mailViewModel.listModel?.selectNone())
return wereDeleted ? ListSwipeDecision.Commit : ListSwipeDecision.Cancel
}
@ -419,7 +423,7 @@ export class MailListView implements Component<MailListViewAttrs> {
this.mailViewModel.listModel?.selectNone()
return ListSwipeDecision.Cancel
} else {
const folders = await locator.mailModel.getMailboxFolders(listElement)
const folders = await mailLocator.mailModel.getMailboxFoldersForMail(listElement)
if (folders) {
//Check if the user is in the trash/spam folder or if it's in Inbox or Archive
//to determinate the target folder
@ -427,7 +431,8 @@ export class MailListView implements Component<MailListViewAttrs> {
? this.getRecoverFolder(listElement, folders)
: assertNotNull(folders.getSystemFolderByType(this.showingArchive ? MailSetKind.INBOX : MailSetKind.ARCHIVE))
const wereMoved = await moveMails({
mailModel: locator.mailModel,
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: [listElement],
targetMailFolder,
})

View file

@ -5,8 +5,9 @@ import m from "mithril"
import { MailReportType, ReportMovedMailsType } from "../../../common/api/common/TutanotaConstants"
import { ButtonAttrs, ButtonType } from "../../../common/gui/base/Button.js"
import { Dialog } from "../../../common/gui/base/Dialog"
import type { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { showSnackBar } from "../../../common/gui/base/SnackBar"
import { MailModel } from "../model/MailModel.js"
function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDetail): Promise<boolean> {
return new Promise((resolve) => {
@ -60,6 +61,7 @@ function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDe
*/
export async function reportMailsAutomatically(
mailReportType: MailReportType,
mailboxModel: MailboxModel,
mailModel: MailModel,
mailboxDetails: MailboxDetail,
mails: ReadonlyArray<Mail>,
@ -68,7 +70,7 @@ export async function reportMailsAutomatically(
return
}
const mailboxProperties = await mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
let allowUndoing = true // decides if a snackbar is shown to prevent the server request
let isReportable = false

View file

@ -20,7 +20,9 @@ import { px, size } from "../../../common/gui/size.js"
import { NBSP, noOp } from "@tutao/tutanota-utils"
import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
import { companyTeamLabel } from "../../../common/misc/ClientConstants.js"
import { getConfidentialFontIcon, getSenderOrRecipientHeading, isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getConfidentialFontIcon, getSenderOrRecipientHeading } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isTutanotaTeamMail } from "./MailGuiUtils.js"
import { mailLocator } from "../../mailLocator.js"
const iconMap: Record<MailSetKind, string> = {
[MailSetKind.CUSTOM]: FontIcons.Folder,
@ -255,7 +257,7 @@ export class MailRow implements VirtualRow<Mail> {
let iconText = ""
if (this.showFolderIcon) {
let folder = locator.mailModel.getMailFolderForMail(mail)
let folder = mailLocator.mailModel.getMailFolderForMail(mail)
iconText += folder ? this.folderIcon(getMailFolderType(folder)) : ""
}

View file

@ -14,7 +14,7 @@ import { keyManager } from "../../../common/misc/KeyManager"
import { getMailSelectionMessage, MultiItemViewer } from "./MultiItemViewer.js"
import { Icons } from "../../../common/gui/base/icons/Icons"
import { showProgressDialog } from "../../../common/gui/dialogs/ProgressDialog"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
import { locator } from "../../../common/api/main/CommonLocator"
import { PermissionError } from "../../../common/api/common/error/PermissionError"
import { styles } from "../../../common/gui/styles"
@ -58,8 +58,9 @@ import { MailFilterButton } from "./MailFilterButton.js"
import { listSelectionKeyboardShortcuts } from "../../../common/gui/base/ListUtils.js"
import { canDoDragAndDropExport, getFolderName, getMailboxName } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { BottomNav } from "../../gui/BottomNav.js"
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
import { showSnackBar } from "../../../common/gui/base/SnackBar.js"
import { isSpamOrTrashFolder } from "../model/MailModel.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode()
@ -227,7 +228,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
vnode.attrs.mailViewModel.init()
this.oncreate = () => {
this.countersStream = locator.mailModel.mailboxCounters.map(m.redraw)
this.countersStream = mailLocator.mailModel.mailboxCounters.map(m.redraw)
keyManager.registerShortcuts(shortcuts)
this.cache.conversationViewPreference = deviceConfig.getConversationViewShowOnlySelectedMail()
}
@ -249,6 +250,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
private mailViewerSingleActions(viewModel: ConversationViewModel) {
return m(MailViewerActions, {
mailboxModel: viewModel.primaryViewModel().mailboxModel,
mailModel: viewModel.primaryViewModel().mailModel,
mailViewerViewModel: viewModel.primaryViewModel(),
mails: [viewModel.primaryMail],
@ -283,7 +285,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
private mailViewerMultiActions() {
return m(MailViewerActions, {
mailModel: locator.mailModel,
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: this.mailViewModel.listModel?.getSelectedAsArray() ?? [],
selectNone: () => this.mailViewModel.listModel?.selectNone(),
})
@ -375,7 +378,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
? m(MobileMailMultiselectionActionBar, {
mails: this.mailViewModel.listModel.getSelectedAsArray(),
selectNone: () => this.mailViewModel.listModel?.selectNone(),
mailModel: locator.mailModel,
mailModel: mailLocator.mailModel,
mailboxModel: locator.mailboxModel,
})
: m(BottomNav),
}),
@ -545,7 +549,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
const selectedMails = mailList.getSelectedAsArray()
showMoveMailsDropdown(locator.mailModel, getMoveMailBounds(), selectedMails)
showMoveMailsDropdown(locator.mailboxModel, mailLocator.mailModel, getMoveMailBounds(), selectedMails)
}
private createFolderColumn(editingFolderForMailGroup: Id | null = null, drawerAttrs: DrawerMenuAttrs) {
@ -577,7 +581,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
}
private renderFolders(editingFolderForMailGroup: Id | null) {
const details = locator.mailModel.mailboxDetails() ?? []
const details = locator.mailboxModel.mailboxDetails() ?? []
return [
...details.map((mailboxDetail) => {
const inEditMode = editingFolderForMailGroup === mailboxDetail.mailGroup._id
@ -601,6 +605,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
private createMailboxFolderItems(mailboxDetail: MailboxDetail, inEditMode: boolean, onEditMailbox: () => void): Children {
return m(MailFoldersView, {
mailModel: mailLocator.mailModel,
mailboxDetail,
expandedFolders: this.expandedState,
mailFolderElementIdToSelectedMailId: this.mailViewModel.getMailFolderToSelectedMail(),
@ -628,7 +633,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
if (location.hash.length > 5) {
let url = location.hash.substring(5)
let decodedUrl = decodeURIComponent(url)
Promise.all([locator.mailModel.getUserMailboxDetails(), import("../editor/MailEditor")]).then(
Promise.all([locator.mailboxModel.getUserMailboxDetails(), import("../editor/MailEditor")]).then(
([mailboxDetails, { newMailtoUrlMailEditor }]) => {
newMailtoUrlMailEditor(decodedUrl, false, mailboxDetails)
.then((editor) => editor.show())
@ -693,7 +698,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
}
}
moveMails({ mailModel: locator.mailModel, mails: mailsToMove, targetMailFolder: folder })
moveMails({ mailboxModel: locator.mailboxModel, mailModel: mailLocator.mailModel, mails: mailsToMove, targetMailFolder: folder })
}
private async showNewMailDialog(): Promise<void> {
@ -713,15 +718,19 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
// remove any selection to avoid that the next mail is loaded and selected for each deleted mail event
this.mailViewModel?.listModel?.selectNone()
if (mailboxDetail.mailbox.folders == null) {
return
}
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
if (isSpamOrTrashFolder(mailboxDetail.folders, folder)) {
if (isSpamOrTrashFolder(folders, folder)) {
const confirmed = await Dialog.confirm(() =>
lang.get("confirmDeleteFinallyCustomFolder_msg", {
"{1}": getFolderName(folder),
}),
)
if (!confirmed) return
await locator.mailModel.finallyDeleteCustomMailFolder(folder)
await mailLocator.mailModel.finallyDeleteCustomMailFolder(folder)
} else {
const confirmed = await Dialog.confirm(() =>
lang.get("confirmDeleteCustomFolder_msg", {
@ -729,7 +738,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
}),
)
if (!confirmed) return
await locator.mailModel.trashFolderAndSubfolders(folder)
await mailLocator.mailModel.trashFolderAndSubfolders(folder)
}
}
@ -742,15 +751,15 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
return
}
// set all selected emails to the opposite of the first email's unread state
await locator.mailModel.markMails(mails, !mails[0].unread)
await mailLocator.mailModel.markMails(mails, !mails[0].unread)
}
private deleteMails(mails: Mail[]): Promise<boolean> {
return promptAndDeleteMails(locator.mailModel, mails, noOp)
return promptAndDeleteMails(mailLocator.mailModel, mails, noOp)
}
private async showFolderAddEditDialog(mailGroupId: Id, folder: MailFolder | null, parentFolder: MailFolder | null) {
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMailGroup(mailGroupId)
const mailboxDetail = await locator.mailboxModel.getMailboxDetailsForMailGroup(mailGroupId)
await showEditFolderDialog(mailboxDetail, folder, parentFolder)
}
}

View file

@ -1,5 +1,5 @@
import { ListModel } from "../../../common/misc/ListModel.js"
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { EntityClient } from "../../../common/api/common/EntityClient.js"
import { Mail, MailFolder, MailSetEntry, MailSetEntryTypeRef, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
import {
@ -45,8 +45,8 @@ import { Router } from "../../../common/gui/ScopedRouter.js"
import { ListFetchResult } from "../../../common/gui/base/ListUtils.js"
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { EventController } from "../../../common/api/main/EventController.js"
import { assertSystemFolderOfType, getMailFilterForType, isOfTypeOrSubfolderOf, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSpamOrTrashFolder, isSubfolderOfType } from "../../../common/api/common/CommonMailUtils.js"
import { getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { assertSystemFolderOfType, isOfTypeOrSubfolderOf, isSpamOrTrashFolder, isSubfolderOfType, MailModel } from "../model/MailModel.js"
import { CacheMode } from "../../../common/api/worker/rest/EntityRestClient.js"
export interface MailOpenedListener {
@ -96,6 +96,7 @@ export class MailViewModel {
private conversationPref: boolean = false
constructor(
private readonly mailboxModel: MailboxModel,
private readonly mailModel: MailModel,
private readonly entityClient: EntityClient,
private readonly eventController: EventController,
@ -126,13 +127,15 @@ export class MailViewModel {
async showMailWithFolderId(folderId?: Id, mailId?: Id): Promise<void> {
if (folderId) {
const mailboxDetails = await this.mailModel.getMailboxDetails()
const mailboxDetail: MailboxDetail | null = mailboxDetails.find((md) => md.folders.getFolderById(folderId)) ?? null
const folder = mailboxDetail?.folders.getFolderById(folderId)
return this.showMail(folder, mailId)
} else {
return this.showMail(null, mailId)
const folderStructures = this.mailModel.folders()
for (const folders of Object.values(folderStructures)) {
const folder = folders.getFolderById(folderId)
if (folder) {
return this.showMail(folder, mailId)
}
}
}
return this.showMail(null, mailId)
}
async showStickyMail(fullMailId: IdTuple, onMissingExplicitMailTarget: () => unknown): Promise<void> {
@ -288,8 +291,9 @@ export class MailViewModel {
}
private async getFolderForUserInbox(): Promise<MailFolder> {
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
return assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX)
const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
const folders = this.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
return assertSystemFolderOfType(folders, MailSetKind.INBOX)
}
init() {
@ -647,7 +651,11 @@ export class MailViewModel {
async switchToFolder(folderType: Omit<MailSetKind, MailSetKind.CUSTOM>): Promise<void> {
const mailboxDetail = assertNotNull(await this.getMailboxDetails())
const folder = assertSystemFolderOfType(mailboxDetail.folders, folderType)
if (mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
return
}
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
const folder = assertSystemFolderOfType(folders, folderType)
await this.showMail(folder, this.mailFolderElementIdToSelectedMailId.get(getElementId(folder)))
}
@ -660,8 +668,9 @@ export class MailViewModel {
if (!this._folder) return false
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(this._folder)
const selectedFolder = this.getFolder()
if (selectedFolder && mailboxDetail) {
return isOfTypeOrSubfolderOf(mailboxDetail.folders, selectedFolder, MailSetKind.DRAFT)
if (selectedFolder && mailboxDetail && mailboxDetail.mailbox.folders) {
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
return isOfTypeOrSubfolderOf(folders, selectedFolder, MailSetKind.DRAFT)
} else {
return false
}
@ -669,16 +678,19 @@ export class MailViewModel {
async showingTrashOrSpamFolder(): Promise<boolean> {
const folder = this.getFolder()
if (!folder) {
return false
if (folder) {
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
if (folder && mailboxDetail && mailboxDetail.mailbox.folders) {
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
return isSpamOrTrashFolder(folders, folder)
}
}
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
return mailboxDetail != null && isSpamOrTrashFolder(mailboxDetail.folders, folder)
return false
}
private async mailboxDetailForListWithFallback(folder?: MailFolder | null) {
const mailboxDetailForListId = folder ? await this.mailModel.getMailboxDetailsForMailFolder(folder) : null
return mailboxDetailForListId ?? (await this.mailModel.getUserMailboxDetails())
return mailboxDetailForListId ?? (await this.mailboxModel.getUserMailboxDetails())
}
async finallyDeleteAllMailsInSelectedFolder(folder: MailFolder): Promise<void> {
@ -694,14 +706,17 @@ export class MailViewModel {
throw new UserError("operationStillActive_msg")
}),
)
} else if (isSubfolderOfType(mailboxDetail.folders, folder, MailSetKind.TRASH) || isSubfolderOfType(mailboxDetail.folders, folder, MailSetKind.SPAM)) {
return this.mailModel.finallyDeleteCustomMailFolder(folder).catch(
ofClass(PreconditionFailedError, () => {
throw new UserError("operationStillActive_msg")
}),
)
} else {
throw new ProgrammingError(`Cannot delete mails in folder ${String(folder._id)} with type ${folder.folderType}`)
const folders = this.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
if (isSubfolderOfType(folders, folder, MailSetKind.TRASH) || isSubfolderOfType(folders, folder, MailSetKind.SPAM)) {
return this.mailModel.finallyDeleteCustomMailFolder(folder).catch(
ofClass(PreconditionFailedError, () => {
throw new UserError("operationStillActive_msg")
}),
)
} else {
throw new ProgrammingError(`Cannot delete mails in folder ${String(folder._id)} with type ${folder.folderType}`)
}
}
}

View file

@ -16,7 +16,7 @@ import { theme } from "../../../common/gui/theme"
import { client } from "../../../common/misc/ClientDetector"
import { styles } from "../../../common/gui/styles"
import { DropdownButtonAttrs, showDropdownAtPosition } from "../../../common/gui/base/Dropdown.js"
import { replaceCidsWithInlineImages } from "./MailGuiUtils"
import { isTutanotaTeamMail, replaceCidsWithInlineImages } from "./MailGuiUtils"
import { getCoordsOfMouseOrTouchEvent } from "../../../common/gui/base/GuiUtils"
import { copyToClipboard } from "../../../common/misc/ClipboardUtils"
import { ContentBlockingStatus, MailViewerViewModel } from "./MailViewerViewModel"
@ -32,7 +32,7 @@ import { locator } from "../../../common/api/main/CommonLocator.js"
import { PinchZoom } from "../../../common/gui/PinchZoom.js"
import { responsiveCardHMargin, responsiveCardHPadding } from "../../../common/gui/cards.js"
import { Dialog } from "../../../common/gui/base/Dialog.js"
import { createNewContact, getExistingRuleForType, isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { createNewContact, getExistingRuleForType } from "../../../common/mailFunctionality/SharedMailUtils.js"
assertMainOrNode()

View file

@ -19,7 +19,7 @@ import { ContentBlockingStatus, MailViewerViewModel } from "./MailViewerViewMode
import { canSeeTutaLinks } from "../../../common/gui/base/GuiUtils.js"
import { isNotNull, noOp, resolveMaybeLazy } from "@tutao/tutanota-utils"
import { IconButton } from "../../../common/gui/base/IconButton.js"
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
import { isTutanotaTeamMail, promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js"
import { editDraft, mailViewerMoreActions } from "./MailViewerUtils.js"
import { liveDataAttrs } from "../../../common/gui/AriaUtils.js"
@ -28,7 +28,6 @@ import { AttachmentBubble, getAttachmentType } from "../../../common/gui/Attachm
import { responsiveCardHMargin, responsiveCardHPadding } from "../../../common/gui/cards.js"
import { companyTeamLabel } from "../../../common/misc/ClientConstants.js"
import { getConfidentialIcon, getFolderIconByType, getMailAddressDisplayText } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { MailAddressAndName } from "../../../common/api/common/CommonMailUtils.js"
export type MailAddressDropdownCreator = (args: {
@ -701,7 +700,8 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
})
actionButtons.push({
label: "move_action",
click: (_: MouseEvent, dom: HTMLElement) => showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
click: (_: MouseEvent, dom: HTMLElement) =>
showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
icon: Icons.Folder,
})
actionButtons.push({
@ -733,7 +733,7 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
actionButtons.push({
label: "move_action",
click: (_: MouseEvent, dom: HTMLElement) =>
showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
icon: Icons.Folder,
})
}

View file

@ -1,5 +1,5 @@
import m, { Children, Component, Vnode } from "mithril"
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { IconButton } from "../../../common/gui/base/IconButton.js"
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
@ -21,11 +21,13 @@ import { ColumnWidth, Table } from "../../../common/gui/base/Table.js"
import { ExpanderButton, ExpanderPanel } from "../../../common/gui/base/Expander.js"
import stream from "mithril/stream"
import { exportMails } from "../export/Exporter.js"
import { MailModel } from "../model/MailModel.js"
/*
note that mailViewerViewModel has a mailModel, so you do not need to pass both if you pass a mailViewerViewModel
*/
export interface MailViewerToolbarAttrs {
mailboxModel: MailboxModel
mailModel: MailModel
mailViewerViewModel?: MailViewerViewModel
mails: Mail[]
@ -50,13 +52,13 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
} else if (attrs.mailViewerViewModel) {
return [
this.renderDeleteButton(mailModel, attrs.mails, attrs.selectNone ?? noOp),
attrs.mailViewerViewModel.canForwardOrMove() ? this.renderMoveButton(mailModel, attrs.mails) : null,
attrs.mailViewerViewModel.canForwardOrMove() ? this.renderMoveButton(attrs.mailboxModel, mailModel, attrs.mails) : null,
attrs.mailViewerViewModel.isDraftMail() ? null : this.renderReadButton(attrs),
]
} else if (attrs.mails.length > 0) {
return [
this.renderDeleteButton(mailModel, attrs.mails, attrs.selectNone ?? noOp),
attrs.mailModel.isMovingMailsAllowed() ? this.renderMoveButton(mailModel, attrs.mails) : null,
attrs.mailModel.isMovingMailsAllowed() ? this.renderMoveButton(attrs.mailboxModel, mailModel, attrs.mails) : null,
this.renderReadButton(attrs),
this.renderExportButton(attrs),
]
@ -94,11 +96,11 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
})
}
private renderMoveButton(mailModel: MailModel, mails: Mail[]): Children {
private renderMoveButton(mailboxModel: MailboxModel, mailModel: MailModel, mails: Mail[]): Children {
return m(IconButton, {
title: "move_action",
icon: Icons.Folder,
click: (e, dom) => showMoveMailsDropdown(mailModel, dom.getBoundingClientRect(), mails),
click: (e, dom) => showMoveMailsDropdown(mailboxModel, mailModel, dom.getBoundingClientRect(), mails),
})
}

View file

@ -21,7 +21,7 @@ import {
OperationType,
} from "../../../common/api/common/TutanotaConstants"
import { EntityClient } from "../../../common/api/common/EntityClient"
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { ContactModel } from "../../../common/contactsFunctionality/ContactModel.js"
import { ConfigurationDatabase } from "../../../common/api/worker/facades/lazy/ConfigurationDatabase.js"
import stream from "mithril/stream"
@ -43,7 +43,7 @@ import { LoginController } from "../../../common/api/main/LoginController"
import m from "mithril"
import { LockedError, NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError"
import { haveSameId, isSameId } from "../../../common/api/common/utils/EntityUtils"
import { getReferencedAttachments, loadInlineImages, moveMails } from "./MailGuiUtils"
import { getReferencedAttachments, isTutanotaTeamMail, loadInlineImages, moveMails } from "./MailGuiUtils"
import { SanitizedFragment } from "../../../common/misc/HtmlSanitizer"
import { CALENDAR_MIME_TYPE, FileController } from "../../../common/file/FileController"
import { exportMails } from "../export/Exporter.js"
@ -69,20 +69,19 @@ import { AttachmentType, getAttachmentType } from "../../../common/gui/Attachmen
import type { ContactImporter } from "../../contacts/ContactImporter.js"
import { InlineImages, revokeInlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
import {
assertSystemFolderOfType,
getDefaultSender,
getEnabledMailAddressesWithUser,
getFolderName,
getMailboxName,
getPathToFolderString,
isNoReplyTeamAddress,
isSystemNotification,
isTutanotaTeamMail,
loadMailDetails,
loadMailHeaders,
} from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSystemNotification, isNoReplyTeamAddress } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../../common/api/common/CommonMailUtils.js"
import { assertSystemFolderOfType, MailModel } from "../model/MailModel.js"
import { CalendarModel } from "../../../calendar-app/calendar/model/CalendarModel.js"
import { mailLocator } from "../../mailLocator.js"
export const enum ContentBlockingStatus {
Block = "0",
@ -137,6 +136,7 @@ export class MailViewerViewModel {
private _mail: Mail,
showFolder: boolean,
readonly entityClient: EntityClient,
public readonly mailboxModel: MailboxModel,
public readonly mailModel: MailModel,
readonly contactModel: ContactModel,
private readonly configFacade: ConfigurationDatabase,
@ -149,7 +149,6 @@ export class MailViewerViewModel {
private readonly mailFacade: MailFacade,
private readonly cryptoFacade: CryptoFacade,
private readonly contactImporter: lazyAsync<ContactImporter>,
private readonly calendarModel: lazyAsync<CalendarModel>,
) {
this.folderMailboxText = null
if (showFolder) {
@ -206,10 +205,11 @@ export class MailViewerViewModel {
if (folder) {
this.mailModel.getMailboxDetailsForMail(this.mail).then((mailboxDetails) => {
if (mailboxDetails == null) {
if (mailboxDetails == null || mailboxDetails.mailbox.folders == null) {
return
}
const name = getPathToFolderString(mailboxDetails.folders, folder)
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetails.mailbox.folders._id)
const name = getPathToFolderString(folders, folder)
this.folderMailboxText = `${getMailboxName(this.logins, mailboxDetails)} / ${name}`
m.redraw()
})
@ -513,12 +513,19 @@ export class MailViewerViewModel {
await this.entityClient.update(this.mail)
}
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(this.mail)
if (mailboxDetail == null) {
if (mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
return
}
const spamFolder = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
const spamFolder = assertSystemFolderOfType(folders, MailSetKind.SPAM)
// do not report moved mails again
await moveMails({ mailModel: this.mailModel, mails: [this.mail], targetMailFolder: spamFolder, isReportable: false })
await moveMails({
mailboxModel: this.mailboxModel,
mailModel: this.mailModel,
mails: [this.mail],
targetMailFolder: spamFolder,
isReportable: false,
})
} catch (e) {
if (e instanceof NotFoundError) {
console.log("mail already moved")
@ -1055,7 +1062,7 @@ export class MailViewerViewModel {
const { importCalendarFile, parseCalendarFile } = await import("../../../common/calendar/import/CalendarImporter.js")
const dataFile = await this.fileController.getAsDataFile(file)
const data = parseCalendarFile(dataFile)
await importCalendarFile(await this.calendarModel(), this.logins.getUserController(), data.contents)
await importCalendarFile(await mailLocator.calendarModel(), this.logins.getUserController(), data.contents)
} catch (e) {
console.log(e)
throw new UserError("errorDuringFileOpen_msg")

View file

@ -16,6 +16,7 @@ import { noOp, promiseMap } from "@tutao/tutanota-utils"
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
import { IconButton } from "../../../common/gui/base/IconButton.js"
import { mailLocator } from "../../mailLocator.js"
const COUNTER_POS_OFFSET = px(-8)
export type MinimizedEditorOverlayAttrs = {
@ -107,7 +108,7 @@ export class MinimizedEditorOverlay implements Component<MinimizedEditorOverlayA
const draft = model.draft
if (draft) {
await promptAndDeleteMails(model.mailModel, [draft], noOp)
await promptAndDeleteMails(mailLocator.mailModel, [draft], noOp)
}
}
})

View file

@ -56,7 +56,7 @@ export class MobileMailActionBar implements Component<MobileMailActionBarAttrs>
return m(IconButton, {
title: "move_action",
click: (e, dom) =>
showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail], {
showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail], {
width: this.dropdownWidth(),
withBackground: true,
}),

View file

@ -5,11 +5,13 @@ import { Icons } from "../../../common/gui/base/icons/Icons.js"
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
import { DROPDOWN_MARGIN } from "../../../common/gui/base/Dropdown.js"
import { MobileBottomActionBar } from "../../../common/gui/MobileBottomActionBar.js"
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { MailModel } from "../model/MailModel.js"
export interface MobileMailMultiselectionActionBarAttrs {
mails: readonly Mail[]
mailModel: MailModel
mailboxModel: MailboxModel
selectNone: () => unknown
}
@ -17,7 +19,7 @@ export class MobileMailMultiselectionActionBar {
private dom: HTMLElement | null = null
view({ attrs }: Vnode<MobileMailMultiselectionActionBarAttrs>): Children {
const { mails, selectNone, mailModel } = attrs
const { mails, selectNone, mailModel, mailboxModel } = attrs
return m(
MobileBottomActionBar,
{
@ -35,7 +37,7 @@ export class MobileMailMultiselectionActionBar {
title: "move_action",
click: (e, dom) => {
const referenceDom = this.dom ?? dom
showMoveMailsDropdown(mailModel, referenceDom.getBoundingClientRect(), mails, {
showMoveMailsDropdown(mailboxModel, mailModel, referenceDom.getBoundingClientRect(), mails, {
onSelected: () => selectNone,
width: referenceDom.offsetWidth - DROPDOWN_MARGIN * 2,
})

View file

@ -1,7 +1,7 @@
import { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js"
import { EventController } from "../common/api/main/EventController.js"
import { SearchModel } from "./search/model/SearchModel.js"
import { MailboxDetail, MailModel } from "../common/mailFunctionality/MailModel.js"
import { type MailboxDetail, MailboxModel } from "../common/mailFunctionality/MailboxModel.js"
import { MinimizedMailEditorViewModel } from "./mail/model/MinimizedMailEditorViewModel.js"
import { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
import { EntityClient } from "../common/api/common/EntityClient.js"
@ -64,7 +64,7 @@ import { SearchViewModel } from "./search/view/SearchViewModel.js"
import { SearchRouter } from "../common/search/view/SearchRouter.js"
import { MailOpenedListener } from "./mail/view/MailViewModel.js"
import { getEnabledMailAddressesWithUser } from "../common/mailFunctionality/SharedMailUtils.js"
import { Const, FeatureType, GroupType, KdfType } from "../common/api/common/TutanotaConstants.js"
import { Const, FeatureType, GroupType, KdfType, MailSetKind } from "../common/api/common/TutanotaConstants.js"
import { ShareableGroupType } from "../common/sharing/GroupUtils.js"
import { ReceivedGroupInvitationsModel } from "../common/sharing/model/ReceivedGroupInvitationsModel.js"
import { CalendarViewModel } from "../calendar-app/calendar/view/CalendarViewModel.js"
@ -116,17 +116,19 @@ import { MobilePaymentsFacade } from "../common/native/common/generatedipc/Mobil
import { AppStorePaymentPicker } from "../common/misc/AppStorePaymentPicker.js"
import { MAIL_PREFIX } from "../common/misc/RouteChange.js"
import { getDisplayedSender } from "../common/api/common/CommonMailUtils.js"
import { assertSystemFolderOfType, isMailInSpamOrTrash, MailModel } from "./mail/model/MailModel.js"
import { AppType } from "../common/misc/ClientConstants.js"
import type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js"
import type { ContactImporter } from "./contacts/ContactImporter.js"
import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js"
import { locator } from "../common/api/main/CommonLocator.js"
import m from "mithril"
assertMainOrNode()
class MailLocator {
eventController!: EventController
search!: SearchModel
mailboxModel!: MailboxModel
mailModel!: MailModel
minimizedMailModel!: MinimizedMailEditorViewModel
contactModel!: ContactModel
@ -224,6 +226,7 @@ class MailLocator {
const conversationViewModelFactory = await this.conversationViewModelFactory()
const router = new ScopedRouter(this.throttledRouter(), "/mail")
return new MailViewModel(
this.mailboxModel,
this.mailModel,
this.entityClient,
this.eventController,
@ -257,7 +260,7 @@ class MailLocator {
searchRouter,
this.search,
this.searchFacade,
this.mailModel,
this.mailboxModel,
this.logins,
this.indexerFacade,
this.entityClient,
@ -325,8 +328,8 @@ class MailLocator {
return new CalendarViewModel(
this.logins,
async (mode: CalendarOperation, event: CalendarEvent) => {
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null)
},
(...args) => this.calendarEventPreviewModel(...args),
@ -338,7 +341,7 @@ class MailLocator {
deviceConfig,
await this.receivedGroupInvitationsModel(GroupType.Calendar),
timeZone,
this.mailModel,
this.mailboxModel,
)
})
@ -359,13 +362,16 @@ class MailLocator {
this.mailFacade,
this.entityClient,
this.logins,
this.mailModel,
this.mailboxModel,
this.contactModel,
this.eventController,
mailboxDetails,
recipientsModel,
dateProvider,
mailboxProperties,
async (mail: Mail) => {
return await isMailInSpamOrTrash(mail)
},
)
}
@ -443,13 +449,14 @@ class MailLocator {
mail,
showFolder,
this.entityClient,
this.mailboxModel,
this.mailModel,
this.contactModel,
this.configFacade,
this.fileController,
this.logins,
async (mailboxDetails) => {
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return this.sendMailModel(mailboxDetails, mailboxProperties)
},
this.eventController,
@ -458,7 +465,6 @@ class MailLocator {
this.mailFacade,
this.cryptoFacade,
() => this.contactImporter(),
() => this.calendarModel(),
)
}
@ -539,7 +545,7 @@ class MailLocator {
async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> {
const { OwnMailAddressNameChanger } = await import("../mail-app/settings/mailaddress/OwnMailAddressNameChanger.js")
return new OwnMailAddressNameChanger(this.mailModel, this.entityClient)
return new OwnMailAddressNameChanger(this.mailboxModel, this.entityClient)
}
async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
@ -685,12 +691,14 @@ class MailLocator {
this.entropyFacade = entropyFacade
this.workerFacade = workerFacade
this.connectivityModel = new WebsocketConnectivityModel(eventBus)
this.mailboxModel = new MailboxModel(this.eventController, this.entityClient, this.logins)
this.mailModel = new MailModel(
notifications,
this.mailboxModel,
this.eventController,
this.mailFacade,
this.entityClient,
this.logins,
this.mailFacade,
this.connectivityModel,
this.inboxRuleHanlder(),
)
@ -729,18 +737,63 @@ class MailLocator {
const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js")
const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.js")
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailModel, MAIL_PREFIX)
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailboxModel, MAIL_PREFIX, async (currentRoute: string) => {
// If the first background column is focused in mail view (showing a folder), move to inbox.
// If in inbox already, quit
const parts = currentRoute.split("/").filter((part) => part !== "")
if (parts.length > 1) {
const selectedMailListId = parts[1]
const [mailboxDetail] = await this.mailboxModel.getMailboxDetails()
const folders = this.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
const inboxMailListId = assertSystemFolderOfType(folders, MailSetKind.INBOX).mails
if (inboxMailListId !== selectedMailListId) {
return MAIL_PREFIX + "/" + inboxMailListId
}
}
return null
})
this.nativeInterfaces = createNativeInterfaces(
this.webMobileFacade,
new WebDesktopFacade(this.logins, async () => this.native),
new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig),
new WebCommonNativeFacade(
this.logins,
this.mailModel,
this.mailboxModel,
this.usageTestController,
async () => this.fileApp,
async () => this.pushService,
this.handleFileImport.bind(this),
async (userId: string, mailAddress: string, requestedPath: string | null) => {
if (mailLocator.logins.isUserLoggedIn() && mailLocator.logins.getUserController().user._id === userId) {
if (!requestedPath) {
const [mailboxDetail] = await mailLocator.mailboxModel.getMailboxDetails()
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
const inbox = assertSystemFolderOfType(folders, MailSetKind.INBOX)
m.route.set("/mail/" + inbox.mails)
} else {
m.route.set("/mail" + requestedPath)
}
} else {
if (!requestedPath) {
m.route.set(`/login?noAutoLogin=false&userId=${userId}&loginWith=${mailAddress}`)
} else {
m.route.set(
`/login?noAutoLogin=false&userId=${userId}&loginWith=${mailAddress}&requestedPath=${encodeURIComponent(requestedPath)}`,
)
}
}
},
async (userId: string) => {
if (mailLocator.logins.isUserLoggedIn() && mailLocator.logins.getUserController().user._id === userId) {
m.route.set("/calendar/agenda")
} else {
m.route.set(`/login?noAutoLogin=false&userId=${userId}&requestedPath=${encodeURIComponent("/calendar/agenda")}`)
}
},
AppType.Integrated,
),
cryptoFacade,
@ -861,7 +914,7 @@ class MailLocator {
this.logins,
this.progressTracker,
this.entityClient,
this.mailModel,
this.mailboxModel,
this.calendarFacade,
this.fileController,
timeZone,
@ -873,7 +926,7 @@ class MailLocator {
readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => {
const { CalendarInviteHandler } = await import("../calendar-app/calendar/view/CalendarInvites.js")
const { calendarNotificationSender } = await import("../calendar-app/calendar/view/CalendarNotificationSender.js")
return new CalendarInviteHandler(this.mailModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
return new CalendarInviteHandler(this.mailboxModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
this.sendMailModel(...arg),
)
})
@ -938,9 +991,9 @@ class MailLocator {
const { getEventType } = await import("../calendar-app/calendar/gui/CalendarGuiUtils.js")
const { CalendarEventPreviewViewModel } = await import("../calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.js")
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const userController = this.logins.getUserController()
const customer = await userController.loadCustomer()
@ -989,7 +1042,7 @@ class MailLocator {
mailLocator.nativeContactsSyncManager()?.syncContacts()
},
async () => {
const calendarModel = await locator.calendarModel()
const calendarModel = await mailLocator.calendarModel()
calendarModel.handleSyncExternalCalendars()
},
)

View file

@ -13,7 +13,7 @@ import { Icon } from "../../common/gui/base/Icon"
import { client } from "../../common/misc/ClientDetector"
import m, { Children, Component, Vnode } from "mithril"
import { theme } from "../../common/gui/theme"
import { getMailFolderIcon } from "../mail/view/MailGuiUtils"
import { getMailFolderIcon, isTutanotaTeamMail } from "../mail/view/MailGuiUtils"
import { locator } from "../../common/api/main/CommonLocator"
import { IndexingErrorReason } from "../../common/api/worker/search/SearchTypes"
import { companyTeamLabel } from "../../common/misc/ClientConstants.js"
@ -21,7 +21,6 @@ import { getTimeZone } from "../../common/calendar/date/CalendarUtils.js"
import { formatEventDuration } from "../../calendar-app/calendar/gui/CalendarGuiUtils.js"
import { getSenderOrRecipientHeading } from "../../common/mailFunctionality/SharedMailUtils.js"
import { isTutanotaTeamMail } from "../../common/mailFunctionality/SharedMailUtils.js"
import { getContactListName } from "../../common/contactsFunctionality/ContactUtils.js"
type SearchBarOverlayAttrs = {

View file

@ -103,6 +103,7 @@ import { getSharedGroupName } from "../../../common/sharing/GroupUtils.js"
import { YEAR_IN_MILLIS } from "@tutao/tutanota-utils/dist/DateUtils.js"
import { getIndentedFolderNameForDropdown } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { BottomNav } from "../../gui/BottomNav.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode()
@ -402,7 +403,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
const conversationViewModel = this.searchViewModel.conversationViewModel
if (this.searchViewModel.listModel?.state.inMultiselect || !conversationViewModel) {
const actions = m(MailViewerActions, {
mailModel: locator.mailModel,
mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: selectedMails,
selectNone: () => this.searchViewModel.listModel.selectNone(),
})
@ -435,6 +437,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
})
} else {
const actions = m(MailViewerActions, {
mailboxModel: conversationViewModel.primaryViewModel().mailboxModel,
mailModel: conversationViewModel.primaryViewModel().mailModel,
mailViewerViewModel: conversationViewModel.primaryViewModel(),
mails: [conversationViewModel.primaryMail],
@ -603,7 +606,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
return m(MobileMailMultiselectionActionBar, {
mails: this.searchViewModel.getSelectedMails(),
selectNone: () => this.searchViewModel.listModel.selectNone(),
mailModel: locator.mailModel,
mailModel: mailLocator.mailModel,
mailboxModel: locator.mailboxModel,
})
} else if (this.viewSlider.focusedColumn === this.resultListColumn) {
return m(
@ -645,8 +649,9 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
]
for (const mailbox of mailboxes) {
const folderStructures = mailLocator.mailModel.folders()
const mailboxIndex = mailboxes.indexOf(mailbox)
const mailFolders = mailbox.folders.getIndentedList()
const mailFolders = folderStructures[assertNotNull(mailbox.mailbox.folders)._id].getIndentedList()
for (const folderInfo of mailFolders) {
if (folderInfo.folder.folderType !== MailSetKind.SPAM) {
const mailboxLabel = mailboxIndex === 0 ? "" : ` (${getGroupInfoDisplayName(mailbox.mailGroupInfo)})`
@ -990,8 +995,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
await showProgressDialog("pleaseWait_msg", calendarInfos)
}
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null)
if (model) {
@ -1027,7 +1032,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
const selectedMails = this.searchViewModel.getSelectedMails()
if (selectedMails.length > 0) {
showMoveMailsDropdown(locator.mailModel, getMoveMailBounds(), selectedMails, {
showMoveMailsDropdown(locator.mailboxModel, mailLocator.mailModel, getMoveMailBounds(), selectedMails, {
onSelected: () => {
if (selectedMails.length > 1) {
this.searchViewModel.listModel.selectNone()
@ -1041,7 +1046,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
let selectedMails = this.searchViewModel.getSelectedMails()
if (selectedMails.length > 0) {
locator.mailModel.markMails(selectedMails, !selectedMails[0].unread)
mailLocator.mailModel.markMails(selectedMails, !selectedMails[0].unread)
}
}
@ -1056,7 +1061,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
this.searchViewModel.listModel.selectNone()
}
locator.mailModel.deleteMails(selected)
mailLocator.mailModel.deleteMails(selected)
}
})
} else if (isSameTypeRef(this.searchViewModel.searchedType, ContactTypeRef)) {
@ -1147,6 +1152,6 @@ function getCurrentSearchMode(): SearchCategoryTypes {
}
async function newMailEditor(): Promise<Dialog> {
const [mailboxDetails, { newMailEditor }] = await Promise.all([locator.mailModel.getUserMailboxDetails(), import("../../mail/editor/MailEditor")])
const [mailboxDetails, { newMailEditor }] = await Promise.all([locator.mailboxModel.getUserMailboxDetails(), import("../../mail/editor/MailEditor")])
return newMailEditor(mailboxDetails)
}

View file

@ -47,7 +47,7 @@ import {
SearchCategoryTypes,
} from "../model/SearchUtils.js"
import Stream from "mithril/stream"
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { SearchFacade } from "../../../common/api/worker/search/SearchFacade.js"
import { LoginController } from "../../../common/api/main/LoginController.js"
import { Indexer } from "../../../common/api/worker/search/Indexer.js"
@ -64,6 +64,7 @@ import { ProgressTracker } from "../../../common/api/main/ProgressTracker.js"
import { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.js"
import { getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getStartOfTheWeekOffsetForUser } from "../../../common/calendar/date/CalendarUtils.js"
import { mailLocator } from "../../mailLocator.js"
const SEARCH_PAGE_SIZE = 100
@ -124,7 +125,7 @@ export class SearchViewModel {
readonly router: SearchRouter,
private readonly search: SearchModel,
private readonly searchFacade: SearchFacade,
private readonly mailModel: MailModel,
private readonly mailboxModel: MailboxModel,
private readonly logins: LoginController,
private readonly indexerFacade: Indexer,
private readonly entityClient: EntityClient,
@ -164,7 +165,7 @@ export class SearchViewModel {
}
})
this.mailboxSubscription = this.mailModel.mailboxDetails.map((mailboxes) => this.onMailboxesChanged(mailboxes))
this.mailboxSubscription = this.mailboxModel.mailboxDetails.map((mailboxes) => this.onMailboxesChanged(mailboxes))
this.eventController.addEntityListener(this.entityEventsListener)
})
@ -552,11 +553,17 @@ export class SearchViewModel {
private onMailboxesChanged(mailboxes: MailboxDetail[]) {
this.mailboxes = mailboxes
const folderStructures = mailLocator.mailModel.folders()
// if selected folder no longer exist select another one
const selectedMailFolder = this.selectedMailFolder
if (selectedMailFolder[0] && mailboxes.every((mailbox) => mailbox.folders.getFolderById(selectedMailFolder[0]) == null)) {
this.selectedMailFolder = [getElementId(assertNotNull(mailboxes[0].folders.getSystemFolderByType(MailSetKind.INBOX)))]
if (
selectedMailFolder[0] &&
mailboxes.every((mailbox) => folderStructures[assertNotNull(mailbox.mailbox.folders)._id].getFolderById(selectedMailFolder[0]) == null)
) {
this.selectedMailFolder = [
getElementId(assertNotNull(folderStructures[assertNotNull(mailboxes[0].mailbox.folders)._id].getSystemFolderByType(MailSetKind.INBOX))),
]
}
}

View file

@ -6,7 +6,7 @@ import { isDomainName, isMailAddress, isRegularExpression } from "../../common/m
import { getInboxRuleTypeNameMapping } from "../mail/model/InboxRuleHandler"
import type { InboxRule } from "../../common/api/entities/tutanota/TypeRefs.js"
import { createInboxRule } from "../../common/api/entities/tutanota/TypeRefs.js"
import type { MailboxDetail } from "../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../common/mailFunctionality/MailboxModel.js"
import stream from "mithril/stream"
import { DropDownSelector } from "../../common/gui/base/DropDownSelector.js"
import { TextField } from "../../common/gui/base/TextField.js"
@ -18,12 +18,14 @@ import { assertMainOrNode } from "../../common/api/common/Env"
import { locator } from "../../common/api/main/CommonLocator"
import { isOfflineError } from "../../common/api/common/utils/ErrorUtils.js"
import {
assertSystemFolderOfType,
getExistingRuleForType,
getFolderName,
getIndentedFolderNameForDropdown,
getPathToFolderString,
} from "../../common/mailFunctionality/SharedMailUtils.js"
import { assertSystemFolderOfType } from "../mail/model/MailModel.js"
import { mailLocator } from "../mailLocator.js"
import type { IndentedFolder } from "../../common/api/common/mail/FolderSystem.js"
assertMainOrNode()
@ -32,8 +34,9 @@ export type InboxRuleTemplate = Pick<InboxRule, "type" | "value"> & { _id?: Inbo
export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemplate) {
if (locator.logins.getUserController().isFreeAccount()) {
showNotAvailableForFreeDialog()
} else if (mailBoxDetail) {
let targetFolders = mailBoxDetail.folders.getIndentedList().map((folderInfo) => {
} else if (mailBoxDetail && mailBoxDetail.mailbox.folders) {
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailBoxDetail.mailbox.folders._id)
let targetFolders = folders.getIndentedList().map((folderInfo: IndentedFolder) => {
return {
name: getIndentedFolderNameForDropdown(folderInfo),
value: folderInfo.folder,
@ -41,8 +44,8 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
})
const inboxRuleType = stream(ruleOrTemplate.type)
const inboxRuleValue = stream(ruleOrTemplate.value)
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : mailBoxDetail.folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder))
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(mailBoxDetail.folders, MailSetKind.ARCHIVE))
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder))
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(folders, MailSetKind.ARCHIVE))
let form = () => [
m(DropDownSelector, {
@ -66,7 +69,7 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
selectedValue: inboxRuleTarget(),
selectedValueDisplay: getFolderName(inboxRuleTarget()),
selectionChangedHandler: inboxRuleTarget,
helpLabel: () => getPathToFolderString(mailBoxDetail.folders, inboxRuleTarget(), true),
helpLabel: () => getPathToFolderString(folders, inboxRuleTarget(), true),
}),
]

View file

@ -9,13 +9,13 @@ import {
TutanotaPropertiesTypeRef,
} from "../../common/api/entities/tutanota/TypeRefs.js"
import { Const, FeatureType, InboxRuleType, OperationType, ReportMovedMailsType } from "../../common/api/common/TutanotaConstants"
import { capitalizeFirstLetter, defer, LazyLoaded, noOp, ofClass } from "@tutao/tutanota-utils"
import { assertNotNull, capitalizeFirstLetter, defer, LazyLoaded, noOp, ofClass } from "@tutao/tutanota-utils"
import { getInboxRuleTypeName } from "../mail/model/InboxRuleHandler"
import { MailAddressTable } from "../../common/settings/mailaddress/MailAddressTable.js"
import { Dialog } from "../../common/gui/base/Dialog"
import { Icons } from "../../common/gui/base/icons/Icons"
import { showProgressDialog } from "../../common/gui/dialogs/ProgressDialog"
import type { MailboxDetail } from "../../common/mailFunctionality/MailModel.js"
import type { MailboxDetail } from "../../common/mailFunctionality/MailboxModel.js"
import { locator } from "../../common/api/main/CommonLocator"
import stream from "mithril/stream"
import Stream from "mithril/stream"
@ -98,7 +98,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
this._mailboxProperties = new LazyLoaded(async () => {
const mailboxGroupRoot = await this.getMailboxGroupRoot()
return mailLocator.mailModel.getMailboxProperties(mailboxGroupRoot)
return mailLocator.mailboxModel.getMailboxProperties(mailboxGroupRoot)
})
this._updateMailboxPropertiesSettings()
@ -120,7 +120,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
private async getMailboxGroupRoot(): Promise<MailboxGroupRoot> {
// For now we assume user mailbox, in the future we should specify which mailbox we are configuring
const { mailboxGroupRoot } = await mailLocator.mailModel.getUserMailboxDetails()
const { mailboxGroupRoot } = await mailLocator.mailboxModel.getUserMailboxDetails()
return mailboxGroupRoot
}
@ -283,7 +283,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
const templateRule = createInboxRuleTemplate(InboxRuleType.RECIPIENT_TO_EQUALS, "")
const addInboxRuleButtonAttrs: IconButtonAttrs = {
title: "addInboxRule_action",
click: () => mailLocator.mailModel.getUserMailboxDetails().then((mailboxDetails) => AddInboxRuleDialog.show(mailboxDetails, templateRule)),
click: () => mailLocator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => AddInboxRuleDialog.show(mailboxDetails, templateRule)),
icon: Icons.Add,
size: ButtonSize.Compact,
}
@ -444,7 +444,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
}
_updateInboxRules(props: TutanotaProperties): void {
mailLocator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
mailLocator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
this._inboxRulesTableLines(
props.inboxRules.map((rule, index) => {
return {
@ -480,7 +480,8 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
}
_getTextForTarget(mailboxDetail: MailboxDetail, targetFolderId: IdTuple): string {
let folder = mailboxDetail.folders.getFolderById(elementIdPart(targetFolderId))
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
let folder = folders.getFolderById(elementIdPart(targetFolderId))
if (folder) {
return getFolderName(folder)

View file

@ -1,12 +1,12 @@
import { AddressToName, MailAddressNameChanger } from "../../../common/settings/mailaddress/MailAddressTableModel.js"
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { createMailAddressProperties, MailboxProperties } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { EntityClient } from "../../../common/api/common/EntityClient.js"
import { findAndRemove } from "@tutao/tutanota-utils"
/** Name changer for personal mailbox of the currently logged-in user. */
export class OwnMailAddressNameChanger implements MailAddressNameChanger {
constructor(private readonly mailModel: MailModel, private readonly entityClient: EntityClient) {}
constructor(private readonly mailboxModel: MailboxModel, private readonly entityClient: EntityClient) {}
async getSenderNames(): Promise<AddressToName> {
const mailboxProperties = await this.getMailboxProperties()
@ -14,8 +14,8 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
}
async setSenderName(address: string, name: string): Promise<AddressToName> {
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
let aliasConfig = mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === address)
if (aliasConfig == null) {
aliasConfig = createMailAddressProperties({ mailAddress: address, senderName: name })
@ -28,8 +28,8 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
}
async removeSenderName(address: string): Promise<AddressToName> {
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
findAndRemove(mailboxProperties.mailAddressProperties, (p) => p.mailAddress === address)
await this.entityClient.update(mailboxProperties)
return this.collectMap(mailboxProperties)
@ -44,7 +44,7 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
}
private async getMailboxProperties(): Promise<MailboxProperties> {
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
return await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
return await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
}
}

View file

@ -5,8 +5,9 @@ import { createTestEntity } from "../../../TestUtils.js"
import { Icons } from "../../../../../src/common/gui/base/icons/Icons.js"
import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError.js"
import { getConfidentialIcon } from "../../../../../src/common/mailFunctionality/SharedMailUtils.js"
import { isSystemNotification, isTutanotaTeamAddress, isTutanotaTeamMail } from "../../../../../src/common/mailFunctionality/SharedMailUtils.js"
import { isSystemNotification } from "../../../../../src/common/mailFunctionality/SharedMailUtils.js"
import { getDisplayedSender } from "../../../../../src/common/api/common/CommonMailUtils.js"
import { isTutanotaTeamAddress, isTutanotaTeamMail } from "../../../../../src/mail-app/mail/view/MailGuiUtils.js"
o.spec("MailUtilsTest", function () {
function createSystemMail(overrides: Partial<Mail> = {}): Mail {

View file

@ -8,7 +8,7 @@ import { getFromMap, remove } from "@tutao/tutanota-utils"
class FakeWindow {
listeners: Map<string, ((e: unknown) => unknown)[]> = new Map()
addEventListener: (typeof Window.prototype)["addEventListener"] = (event, listener) => {
addEventListener: typeof Window.prototype["addEventListener"] = (event, listener) => {
this.getListeners(event).push(listener)
}
@ -16,7 +16,7 @@ class FakeWindow {
return getFromMap(this.listeners, event, () => [])
}
removeEventListener: (typeof Window.prototype)["removeEventListener"] = (event, listener) => {
removeEventListener: typeof Window.prototype["removeEventListener"] = (event, listener) => {
remove(this.getListeners(event), listener)
}
@ -26,7 +26,7 @@ class FakeWindow {
}
}
crypto: Partial<(typeof Window.prototype)["crypto"]> = {
crypto: Partial<typeof Window.prototype["crypto"]> = {
getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (array) {
array[0] = 32

View file

@ -18,20 +18,25 @@ import { findAttendeeInAddresses } from "../../../src/common/api/common/utils/Co
import { instance, matchers, when } from "testdouble"
import { CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js"
import { LoginController } from "../../../src/common/api/main/LoginController.js"
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
import { GroupInfoTypeRef, GroupTypeRef, User } from "../../../src/common/api/entities/sys/TypeRefs.js"
import { calendars, makeUserController } from "./CalendarTestUtils.js"
import { UserController } from "../../../src/common/api/main/UserController.js"
import { CalendarNotificationSender } from "../../../src/calendar-app/calendar/view/CalendarNotificationSender.js"
import { mockAttribute } from "@tutao/tutanota-test-utils"
import { SendMailModel } from "../../../src/common/mailFunctionality/SendMailModel.js"
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
const { anything, argThat } = matchers
o.spec("CalendarInviteHandlerTest", function () {
let mailModel: MailModel, calendarIniviteHandler: CalendarInviteHandler, calendarModel: CalendarModel, logins: LoginController, sendMailModel: SendMailModel
let maiboxModel: MailboxModel,
calendarIniviteHandler: CalendarInviteHandler,
calendarModel: CalendarModel,
logins: LoginController,
sendMailModel: SendMailModel
let calendarNotificationSender: CalendarNotificationSender
let mailboxDetails: MailboxDetail
o.beforeEach(function () {
const customerId = "customerId"
@ -42,7 +47,7 @@ o.spec("CalendarInviteHandlerTest", function () {
const userSettingsGroupRoot = createTestEntity(UserSettingsGroupRootTypeRef)
let userController: Partial<UserController> = makeUserController([], AccountType.FREE, undefined, false, false, user, userSettingsGroupRoot)
const mailboxDetails: MailboxDetail = {
mailboxDetails = {
mailbox: createTestEntity(MailBoxTypeRef),
folders: new FolderSystem([]),
mailGroupInfo: createTestEntity(GroupInfoTypeRef, {
@ -53,9 +58,8 @@ o.spec("CalendarInviteHandlerTest", function () {
}
const mailboxProperties: MailboxProperties = createTestEntity(MailboxPropertiesTypeRef, {})
mailModel = instance(MailModel)
when(mailModel.getMailboxDetailsForMail(anything())).thenResolve(mailboxDetails)
when(mailModel.getMailboxProperties(anything())).thenResolve(mailboxProperties)
maiboxModel = instance(MailboxModel)
when(maiboxModel.getMailboxProperties(anything())).thenResolve(mailboxProperties)
calendarModel = instance(CalendarModel)
when(calendarModel.getEventsByUid(anything())).thenResolve({
@ -73,7 +77,7 @@ o.spec("CalendarInviteHandlerTest", function () {
sendMailModel = instance(SendMailModel)
calendarIniviteHandler = new CalendarInviteHandler(mailModel, calendarModel, logins, calendarNotificationSender, async () => {
calendarIniviteHandler = new CalendarInviteHandler(maiboxModel, calendarModel, logins, calendarNotificationSender, async () => {
return sendMailModel
})
})
@ -103,7 +107,9 @@ o.spec("CalendarInviteHandlerTest", function () {
let mail = createTestEntity(MailTypeRef)
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
when(calendarModel.getCalendarInfos()).thenResolve(calendars)
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.ACCEPTED, mail)).equals(ReplyResult.ReplySent)
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.ACCEPTED, mail, mailboxDetails)).equals(
ReplyResult.ReplySent,
)
o(calendarModel.processCalendarEventMessage.callCount).equals(1)
})
@ -131,7 +137,9 @@ o.spec("CalendarInviteHandlerTest", function () {
let mail = createTestEntity(MailTypeRef)
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
when(calendarModel.getCalendarInfos()).thenResolve(calendars)
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail)).equals(ReplyResult.ReplySent)
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail, mailboxDetails)).equals(
ReplyResult.ReplySent,
)
o(calendarModel.processCalendarEventMessage.callCount).equals(0)
})
@ -160,7 +168,9 @@ o.spec("CalendarInviteHandlerTest", function () {
let mail = createTestEntity(MailTypeRef)
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
when(calendarModel.getCalendarInfos()).thenResolve(new Map())
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail)).equals(ReplyResult.ReplySent)
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail, mailboxDetails)).equals(
ReplyResult.ReplySent,
)
o(calendarModel.processCalendarEventMessage.callCount).equals(0)
})
})

View file

@ -33,7 +33,7 @@ import { createTestEntity } from "../TestUtils.js"
import { NoopProgressMonitor } from "../../../src/common/api/common/utils/ProgressMonitor.js"
import { makeAlarmScheduler } from "./CalendarTestUtils.js"
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.js"
import { MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
import { incrementByRepeatPeriod } from "../../../src/common/calendar/date/CalendarUtils.js"
import { ExternalCalendarFacade } from "../../../src/common/native/common/generatedipc/ExternalCalendarFacade.js"
import { DeviceConfig } from "../../../src/common/misc/DeviceConfig.js"
@ -728,7 +728,7 @@ function makeLoginController(): LoginController {
return loginController
}
function makeMailModel(): MailModel {
function makeMailModel(): MailboxModel {
return downcast({})
}

View file

@ -34,7 +34,7 @@ import { Recipient, RecipientType } from "../../../src/common/api/common/recipie
import { DateTime } from "luxon"
import { createTestEntity } from "../TestUtils.js"
import { matchers, object, when } from "testdouble"
import { MailboxDetail } from "../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail } from "../../../src/common/mailFunctionality/MailboxModel.js"
import { AlarmScheduler } from "../../../src/common/calendar/date/AlarmScheduler.js"
export const ownerMailAddress = "calendarowner@tutanota.de" as const
@ -250,7 +250,6 @@ export function makeUserController(
export function makeMailboxDetail(): MailboxDetail {
return {
mailbox: createTestEntity(MailBoxTypeRef),
folders: new FolderSystem([]),
mailGroupInfo: createTestEntity(GroupInfoTypeRef),
mailGroup: createTestEntity(GroupTypeRef, {
user: ownerId,

View file

@ -24,7 +24,7 @@ import {
} from "../../../src/calendar-app/calendar/view/CalendarViewModel.js"
import { CalendarInfo, CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js"
import { CalendarEventsRepository, DaysToEvents } from "../../../src/common/calendar/date/CalendarEventsRepository.js"
import { MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
import { MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
import { addDaysForEventInstance, getMonthRange } from "../../../src/common/calendar/date/CalendarUtils.js"
import { CalendarEventModel, CalendarOperation, EventSaveResult } from "../../../src/calendar-app/calendar/gui/eventeditor-model/CalendarEventModel.js"
@ -72,7 +72,7 @@ o.spec("CalendarViewModel", function () {
getUserController: () => userController,
isInternalUserLoggedIn: () => true,
})
const mailModel: MailModel = object()
const mailboxModel: MailboxModel = object()
const previewModelFactory: CalendarEventPreviewModelFactory = async () => object()
const viewModel = new CalendarViewModel(
loginController,
@ -86,7 +86,7 @@ o.spec("CalendarViewModel", function () {
deviceConfig,
calendarInvitations,
zone,
mailModel,
mailboxModel,
)
viewModel.allowDrag = () => true
return { viewModel, calendarModel, eventsRepository }

View file

@ -40,7 +40,7 @@ import { createTestEntity } from "../../TestUtils.js"
import { areExcludedDatesEqual, areRepeatRulesEqual } from "../../../../src/common/calendar/date/CalendarUtils.js"
import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js"
import { FolderSystem } from "../../../../src/common/api/common/mail/FolderSystem.js"
import { MailboxDetail } from "../../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail } from "../../../../src/common/mailFunctionality/MailboxModel.js"
o.spec("CalendarEventModelTest", function () {
let userController: UserController

View file

@ -6,24 +6,20 @@ import { MailSetKind, OperationType } from "../../../src/common/api/common/Tutan
import { MailFolderTypeRef, MailTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
import { EntityClient } from "../../../src/common/api/common/EntityClient.js"
import { EntityRestClientMock } from "../api/worker/rest/EntityRestClientMock.js"
import nodemocker from "../nodemocker.js"
import { downcast } from "@tutao/tutanota-utils"
import { MailFacade } from "../../../src/common/api/worker/facades/lazy/MailFacade.js"
import { LoginController } from "../../../src/common/api/main/LoginController.js"
import { matchers, object, when } from "testdouble"
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
import { WebsocketConnectivityModel } from "../../../src/common/misc/WebsocketConnectivityModel.js"
import { UserController } from "../../../src/common/api/main/UserController.js"
import { createTestEntity } from "../TestUtils.js"
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.js"
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
import { InboxRuleHandler } from "../../../src/mail-app/mail/model/InboxRuleHandler.js"
import { getElementId, getListId } from "../../../src/common/api/common/utils/EntityUtils.js"
o.spec("MailModelTest", function () {
let notifications: Partial<Notifications>
let showSpy: Spy
let model: MailModel
let model: MailboxModel
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"], isMailSet: false })
inboxFolder.mails = "instanceListId"
inboxFolder.folderType = MailSetKind.INBOX
@ -36,22 +32,15 @@ o.spec("MailModelTest", function () {
const restClient: EntityRestClientMock = new EntityRestClientMock()
o.beforeEach(function () {
mailboxDetails = [
{
folders: new FolderSystem([inboxFolder, anotherFolder]),
},
]
notifications = {}
showSpy = notifications.showNotification = spy()
const connectivityModel = object<WebsocketConnectivityModel>()
const mailFacade = nodemocker.mock<MailFacade>("mailFacade", {}).set()
logins = object()
let userController = object<UserController>()
when(userController.isUpdateForLoggedInUserInstance(matchers.anything(), matchers.anything())).thenReturn(false)
when(logins.getUserController()).thenReturn(userController)
inboxRuleHandler = object()
model = new MailModel(downcast(notifications), downcast({}), mailFacade, new EntityClient(restClient), logins, connectivityModel, inboxRuleHandler)
model = new MailboxModel(downcast({}), new EntityClient(restClient), logins)
// not pretty, but works
model.mailboxDetails(mailboxDetails as MailboxDetail[])
})

View file

@ -11,6 +11,7 @@ import {
ConversationEntryTypeRef,
createContact,
CustomerAccountCreateDataTypeRef,
Mail,
MailAddressTypeRef,
MailboxGroupRootTypeRef,
MailboxPropertiesTypeRef,
@ -45,7 +46,7 @@ import { NoZoneDateProvider } from "../../../src/common/api/common/utils/NoZoneD
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
import { createTestEntity } from "../TestUtils.js"
import { ContactModel } from "../../../src/common/contactsFunctionality/ContactModel.js"
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
import { SendMailModel, TOO_MANY_VISIBLE_RECIPIENTS } from "../../../src/common/mailFunctionality/SendMailModel.js"
import { RecipientField } from "../../../src/common/mailFunctionality/SharedMailUtils.js"
import { getContactDisplayName } from "../../../src/common/contactsFunctionality/ContactUtils.js"
@ -95,7 +96,7 @@ o.spec("SendMailModel", function () {
lang.init(en)
})
let mailModel: MailModel, entity: EntityClient, mailFacade: MailFacade, recipientsModel: RecipientsModel
let mailboxModel: MailboxModel, entity: EntityClient, mailFacade: MailFacade, recipientsModel: RecipientsModel
let model: SendMailModel
@ -109,7 +110,7 @@ o.spec("SendMailModel", function () {
).thenDo(() => ({ contacts: testIdGenerator.newId() }))
when(entity.load(anything(), anything(), anything())).thenDo((typeRef, id, params) => ({ _type: typeRef, _id: id }))
mailModel = instance(MailModel)
mailboxModel = instance(MailboxModel)
const contactModel = object<ContactModel>()
when(contactModel.getContactListId()).thenResolve("contactListId")
@ -153,7 +154,6 @@ o.spec("SendMailModel", function () {
const mailboxDetails: MailboxDetail = {
mailbox: createTestEntity(MailBoxTypeRef),
folders: new FolderSystem([]),
mailGroupInfo: createTestEntity(GroupInfoTypeRef, {
mailAddress: "mailgroup@addre.ss",
}),
@ -180,13 +180,16 @@ o.spec("SendMailModel", function () {
mailFacade,
entity,
loginController,
mailModel,
mailboxModel,
contactModel,
eventController,
mailboxDetails,
recipientsModel,
new NoZoneDateProvider(),
mailboxProperties,
async (mail: Mail) => {
return false
},
)
replace(model, "getDefaultSender", () => DEFAULT_SENDER_FOR_TESTING)

View file

@ -19,7 +19,8 @@ import { matchers, object, when } from "testdouble"
import { MailSetKind, MailState, OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
import { isSameId } from "../../../../src/common/api/common/utils/EntityUtils.js"
import { createTestEntity } from "../../TestUtils.js"
import { MailboxDetail, MailModel } from "../../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../../src/common/mailFunctionality/MailboxModel.js"
import { MailModel } from "../../../../src/mail-app/mail/model/MailModel.js"
o.spec("ConversationViewModel", function () {
let conversation: ConversationEntry[]
@ -29,6 +30,7 @@ o.spec("ConversationViewModel", function () {
let viewModel: ConversationViewModel
let mailModel: MailModel
let mailboxModel: MailboxModel
let mailboxDetail: MailboxDetail
let entityRestClientMock: EntityRestClientMock
let prefProvider: ConversationPrefProvider

View file

@ -23,10 +23,10 @@ import { MailState } from "../../../../src/common/api/common/TutanotaConstants.j
import { GroupInfoTypeRef } from "../../../../src/common/api/entities/sys/TypeRefs.js"
import { CryptoFacade } from "../../../../src/common/api/worker/crypto/CryptoFacade.js"
import { ContactImporter } from "../../../../src/mail-app/contacts/ContactImporter.js"
import { MailboxDetail, MailModel } from "../../../../src/common/mailFunctionality/MailModel.js"
import { MailboxDetail, MailboxModel } from "../../../../src/common/mailFunctionality/MailboxModel.js"
import { ContactModel } from "../../../../src/common/contactsFunctionality/ContactModel.js"
import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js"
import { CalendarModel } from "../../../../src/calendar-app/calendar/model/CalendarModel.js"
import { MailModel } from "../../../../src/mail-app/mail/model/MailModel.js"
o.spec("MailViewerViewModel", function () {
let mail: Mail
@ -34,6 +34,7 @@ o.spec("MailViewerViewModel", function () {
let entityClient: EntityClient
let mailModel: MailModel
let mailboxModel: MailboxModel
let contactModel: ContactModel
let configFacade: ConfigurationDatabase
let fileController: FileController
@ -46,11 +47,11 @@ o.spec("MailViewerViewModel", function () {
let sendMailModelFactory: (mailboxDetails: MailboxDetail) => Promise<SendMailModel> = () => Promise.resolve(sendMailModel)
let cryptoFacade: CryptoFacade
let contactImporter: ContactImporter
let calendarModel: CalendarModel
function makeViewModelWithHeaders(headers: string) {
entityClient = object()
mailModel = object()
mailboxModel = object()
contactModel = object()
configFacade = object()
fileController = object()
@ -62,13 +63,13 @@ o.spec("MailViewerViewModel", function () {
mailFacade = object()
cryptoFacade = object()
contactImporter = object()
calendarModel = object()
mail = prepareMailWithHeaders(mailFacade, headers)
return new MailViewerViewModel(
mail,
showFolder,
entityClient,
mailboxModel,
mailModel,
contactModel,
configFacade,
@ -81,7 +82,6 @@ o.spec("MailViewerViewModel", function () {
mailFacade,
cryptoFacade,
async () => contactImporter,
async () => calendarModel,
)
}