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", 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) { export function fancy(text: string, code: CodeValues) {
if (typeof process !== "undefined" && process.stdout.isTTY) { 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 { convertToDataFile, DataFile } from "../../../common/api/common/DataFile"
import type { DateWrapper, RepeatRule, UserAlarmInfo } from "../../../common/api/entities/sys/TypeRefs.js" import type { DateWrapper, RepeatRule, UserAlarmInfo } from "../../../common/api/entities/sys/TypeRefs.js"
import { DateTime } from "luxon" import { DateTime } from "luxon"
import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController"
import { getLetId } from "../../../common/api/common/utils/EntityUtils" 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 */ /** 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 { export function makeInvitationCalendarFile(event: CalendarEvent, method: CalendarMethod, now: Date, zone: string): DataFile {

View file

@ -65,7 +65,7 @@ import {
MailboxProperties, MailboxProperties,
} from "../../../../common/api/entities/tutanota/TypeRefs.js" } from "../../../../common/api/entities/tutanota/TypeRefs.js"
import { User } from "../../../../common/api/entities/sys/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 { import {
AlarmInterval, AlarmInterval,
areRepeatRulesEqual, areRepeatRulesEqual,

View file

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

View file

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

View file

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

View file

@ -598,8 +598,8 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
await showProgressDialog("pleaseWait_msg", calendarInfos) await showProgressDialog("pleaseWait_msg", calendarInfos)
} }
const mailboxDetails = await locator.mailModel.getUserMailboxDetails() const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null) const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null)
if (model) { if (model) {
await showNewCalendarEventEditDialog(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 { CalendarEventsRepository, DaysToEvents } from "../../../common/calendar/date/CalendarEventsRepository.js"
import { CalendarEventPreviewViewModel } from "../gui/eventpopup/CalendarEventPreviewViewModel.js" import { CalendarEventPreviewViewModel } from "../gui/eventpopup/CalendarEventPreviewViewModel.js"
import { EntityUpdateData, isUpdateFor, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.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" import { getEnabledMailAddressesWithUser } from "../../../common/mailFunctionality/SharedMailUtils.js"
export type EventsOnDays = { export type EventsOnDays = {
@ -91,7 +91,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
private readonly deviceConfig: DeviceConfig, private readonly deviceConfig: DeviceConfig,
private readonly calendarInvitationsModel: ReceivedGroupInvitationsModel<GroupType.Calendar>, private readonly calendarInvitationsModel: ReceivedGroupInvitationsModel<GroupType.Calendar>,
private readonly timeZone: string, private readonly timeZone: string,
private readonly mailModel: MailModel, private readonly mailboxModel: MailboxModel,
) { ) {
this._transientEvents = [] this._transientEvents = []
@ -197,7 +197,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
private canFullyEditEvent(event: CalendarEvent): boolean { private canFullyEditEvent(event: CalendarEvent): boolean {
const userController = this.logins.getUserController() const userController = this.logins.getUserController()
const userMailGroup = userController.getUserMailGroupMembership().group 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 mailboxDetails = assertNotNull(mailboxDetailsArray.find((md) => md.mailGroup._id === userMailGroup))
const ownMailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userController.userGroupInfo) const ownMailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userController.userGroupInfo)
const eventType = getEventType(event, this.calendarInfos, ownMailAddresses, userController) 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 { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js"
import { EventController } from "../common/api/main/EventController.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 { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
import { EntityClient } from "../common/api/common/EntityClient.js" import { EntityClient } from "../common/api/common/EntityClient.js"
import { ProgressTracker } from "../common/api/main/ProgressTracker.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 type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js"
import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js" import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js"
import { locator } from "../common/api/main/CommonLocator.js" import { locator } from "../common/api/main/CommonLocator.js"
import m from "mithril"
assertMainOrNode() assertMainOrNode()
class CalendarLocator { class CalendarLocator {
eventController!: EventController eventController!: EventController
search!: CalendarSearchModel search!: CalendarSearchModel
mailModel!: MailModel mailboxModel!: MailboxModel
contactModel!: ContactModel contactModel!: ContactModel
entityClient!: EntityClient entityClient!: EntityClient
progressTracker!: ProgressTracker progressTracker!: ProgressTracker
@ -269,8 +270,8 @@ class CalendarLocator {
return new CalendarViewModel( return new CalendarViewModel(
this.logins, this.logins,
async (mode: CalendarOperation, event: CalendarEvent) => { async (mode: CalendarOperation, event: CalendarEvent) => {
const mailboxDetail = await this.mailModel.getUserMailboxDetails() const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot) const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null) return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null)
}, },
(...args) => this.calendarEventPreviewModel(...args), (...args) => this.calendarEventPreviewModel(...args),
@ -282,7 +283,7 @@ class CalendarLocator {
deviceConfig, deviceConfig,
await this.receivedGroupInvitationsModel(GroupType.Calendar), await this.receivedGroupInvitationsModel(GroupType.Calendar),
timeZone, timeZone,
this.mailModel, this.mailboxModel,
) )
}) })
@ -303,13 +304,16 @@ class CalendarLocator {
this.mailFacade, this.mailFacade,
this.entityClient, this.entityClient,
this.logins, this.logins,
this.mailModel, this.mailboxModel,
this.contactModel, this.contactModel,
this.eventController, this.eventController,
mailboxDetails, mailboxDetails,
recipientsModel, recipientsModel,
dateProvider, dateProvider,
mailboxProperties, mailboxProperties,
async (mail: Mail) => {
return false
},
) )
} }
@ -425,7 +429,7 @@ class CalendarLocator {
async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> { async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> {
const { OwnMailAddressNameChanger } = await import("../mail-app/settings/mailaddress/OwnMailAddressNameChanger.js") 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> { async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
@ -571,7 +575,7 @@ class CalendarLocator {
this.entropyFacade = entropyFacade this.entropyFacade = entropyFacade
this.workerFacade = workerFacade this.workerFacade = workerFacade
this.connectivityModel = new WebsocketConnectivityModel(eventBus) 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.operationProgressTracker = new OperationProgressTracker()
this.infoMessageHandler = new InfoMessageHandler((state: SearchIndexStateInfo) => { this.infoMessageHandler = new InfoMessageHandler((state: SearchIndexStateInfo) => {
// calendar does not have index, so nothing needs to be handled here // 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 { WebInterWindowEventFacade } = await import("../common/native/main/WebInterWindowEventFacade.js")
const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js") const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js")
const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.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.nativeInterfaces = createNativeInterfaces(
this.webMobileFacade, this.webMobileFacade,
new WebDesktopFacade(this.logins, async () => this.native), new WebDesktopFacade(this.logins, async () => this.native),
new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig), new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig),
new WebCommonNativeFacade( new WebCommonNativeFacade(
this.logins, this.logins,
this.mailModel, this.mailboxModel,
this.usageTestController, this.usageTestController,
async () => this.fileApp, async () => this.fileApp,
async () => this.pushService, async () => this.pushService,
this.handleFileImport.bind(this), 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, AppType.Calendar,
), ),
cryptoFacade, cryptoFacade,
@ -763,7 +775,7 @@ class CalendarLocator {
this.logins, this.logins,
this.progressTracker, this.progressTracker,
this.entityClient, this.entityClient,
this.mailModel, this.mailboxModel,
this.calendarFacade, this.calendarFacade,
this.fileController, this.fileController,
timeZone, timeZone,
@ -775,7 +787,7 @@ class CalendarLocator {
readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => { readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => {
const { CalendarInviteHandler } = await import("./calendar/view/CalendarInvites.js") const { CalendarInviteHandler } = await import("./calendar/view/CalendarInvites.js")
const { calendarNotificationSender } = await import("./calendar/view/CalendarNotificationSender.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), this.sendMailModel(...arg),
) )
}) })
@ -797,9 +809,9 @@ class CalendarLocator {
const { getEventType } = await import("./calendar/gui/CalendarGuiUtils.js") const { getEventType } = await import("./calendar/gui/CalendarGuiUtils.js")
const { CalendarEventPreviewViewModel } = await import("./calendar/gui/eventpopup/CalendarEventPreviewViewModel.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 userController = this.logins.getUserController()
const customer = await userController.loadCustomer() const customer = await userController.loadCustomer()

View file

@ -1201,3 +1201,4 @@ export enum GroupKeyRotationType {
export const GroupKeyRotationTypeNameByCode = reverse(GroupKeyRotationType) export const GroupKeyRotationTypeNameByCode = reverse(GroupKeyRotationType)
export const EXTERNAL_CALENDAR_SYNC_INTERVAL = 60 * 30 * 1000 // 30 minutes 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 { WorkerFacade } from "../worker/facades/WorkerFacade.js"
import { WorkerRandomizer } from "../worker/WorkerImpl.js" import { WorkerRandomizer } from "../worker/WorkerImpl.js"
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.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 { EventController } from "./EventController.js"
import type { ContactModel } from "../../contactsFunctionality/ContactModel.js" import type { ContactModel } from "../../contactsFunctionality/ContactModel.js"
import { ProgressTracker } from "./ProgressTracker.js" import { ProgressTracker } from "./ProgressTracker.js"
@ -115,7 +115,7 @@ export interface CommonLocator {
random: WorkerRandomizer random: WorkerRandomizer
connectivityModel: WebsocketConnectivityModel connectivityModel: WebsocketConnectivityModel
mailModel: MailModel mailboxModel: MailboxModel
calendarModel(): Promise<CalendarModel> 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 { AssociationType, Cardinality, Type as TypeId, ValueType } from "../../common/EntityConstants.js"
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js" import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
import { sql, SqlFragment } from "./Sql.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 * 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 { containsEventOfType, EntityUpdateData } from "../../common/utils/EntityUpdateUtils.js"
import { b64UserIdHash } from "./DbFacade.js" import { b64UserIdHash } from "./DbFacade.js"
import { hasError } from "../../common/utils/ErrorUtils.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 export const INITIAL_MAIL_INDEX_INTERVAL_DAYS = 28
const ENTITY_INDEXER_CHUNK = 20 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 */ /** the single source of truth for this configuration */
export const SUPPORTED_MODES = Object.freeze([CredentialEncryptionMode.DEVICE_LOCK, CredentialEncryptionMode.APP_PASSWORD] as const) 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) { export function assertSupportedEncryptionMode(encryptionMode: DesktopCredentialsMode) {
assert(SUPPORTED_MODES.includes(encryptionMode), `should not use unsupported encryption mode ${encryptionMode}`) 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 { getSafeAreaInsetBottom } from "./HtmlUtils.js"
import { hasError } from "../api/common/utils/ErrorUtils.js" import { hasError } from "../api/common/utils/ErrorUtils.js"
import { BubbleButton, bubbleButtonHeight, bubbleButtonPadding } from "./base/buttons/BubbleButton.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 { BootIcons } from "./base/icons/BootIcons.js"
import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js"
export enum AttachmentType { export enum AttachmentType {
GENERIC, GENERIC,

View file

@ -152,7 +152,7 @@ export class PostLoginActions implements PostLoginAction {
if (!isAdminClient()) { 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 // 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() const calendarModel = await locator.calendarModel()
await calendarModel.init() await calendarModel.init()
await this.remindActiveOutOfOfficeNotification() 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 { createApprovalMail } from "../api/entities/monitor/TypeRefs.js"
import { CustomerPropertiesTypeRef } from "../api/entities/sys/TypeRefs.js" import { CustomerPropertiesTypeRef } from "../api/entities/sys/TypeRefs.js"
import { isMailAddress } from "../misc/FormatValidator.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 { ContactModel } from "../contactsFunctionality/ContactModel.js"
import { getContactDisplayName } from "../contactsFunctionality/ContactUtils.js" import { getContactDisplayName } from "../contactsFunctionality/ContactUtils.js"
import { getMailBodyText } from "../api/common/CommonMailUtils.js" import { getMailBodyText } from "../api/common/CommonMailUtils.js"
@ -152,13 +152,14 @@ export class SendMailModel {
public readonly mailFacade: MailFacade, public readonly mailFacade: MailFacade,
public readonly entity: EntityClient, public readonly entity: EntityClient,
public readonly logins: LoginController, public readonly logins: LoginController,
public readonly mailModel: MailModel, public readonly mailboxModel: MailboxModel,
public readonly contactModel: ContactModel, public readonly contactModel: ContactModel,
private readonly eventController: EventController, private readonly eventController: EventController,
public readonly mailboxDetails: MailboxDetail, public readonly mailboxDetails: MailboxDetail,
private readonly recipientsModel: RecipientsModel, private readonly recipientsModel: RecipientsModel,
private readonly dateProvider: DateProvider, private readonly dateProvider: DateProvider,
private mailboxProperties: MailboxProperties, private mailboxProperties: MailboxProperties,
private readonly needNewDraft: (mail: Mail) => Promise<boolean>,
) { ) {
const userProps = logins.getUserController().props const userProps = logins.getUserController().props
this.senderAddress = this.getDefaultSender() this.senderAddress = this.getDefaultSender()
@ -885,7 +886,7 @@ export class SendMailModel {
}).html }).html
this.draft = 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.createDraft(body, attachments, mailMethod)
: await this.updateDraft(body, attachments, this.draft) : 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> { private sendApprovalMail(body: string): Promise<unknown> {
const listId = "---------c--" const listId = "---------c--"
const m = createApprovalMail({ const m = createApprovalMail({

View file

@ -14,7 +14,7 @@ import {
TutanotaProperties, TutanotaProperties,
} from "../api/entities/tutanota/TypeRefs.js" } from "../api/entities/tutanota/TypeRefs.js"
import { fullNameToFirstAndLastName, mailAddressToFirstAndLastName } from "../misc/parsing/MailAddressParser.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 { import {
ContactAddressType, ContactAddressType,
ConversationType, ConversationType,
@ -33,17 +33,17 @@ import { getEnabledMailAddressesForGroupInfo, getGroupInfoDisplayName } from "..
import { lang, Language, TranslationKey } from "../misc/LanguageViewModel.js" import { lang, Language, TranslationKey } from "../misc/LanguageViewModel.js"
import { AllIcons } from "../gui/base/Icon.js" import { AllIcons } from "../gui/base/Icon.js"
import { Icons } from "../gui/base/icons/Icons.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 { LoginController } from "../api/main/LoginController.js"
import { EntityClient } from "../api/common/EntityClient.js" import { EntityClient } from "../api/common/EntityClient.js"
import { getListId, isSameId } from "../api/common/utils/EntityUtils.js" import type { FolderSystem } from "../api/common/mail/FolderSystem.js"
import type { FolderSystem, IndentedFolder } from "../api/common/mail/FolderSystem.js"
import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js" import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js"
import { ListFilter } from "../misc/ListModel.js" import { ListFilter } from "../misc/ListModel.js"
import { FontIcons } from "../gui/base/icons/FontIcons.js" import { FontIcons } from "../gui/base/icons/FontIcons.js"
import { ProgrammingError } from "../api/common/error/ProgrammingError.js" import { ProgrammingError } from "../api/common/error/ProgrammingError.js"
import { Attachment } from "./SendMailModel.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() assertMainOrNode()
export const LINE_BREAK = "<br>" export const LINE_BREAK = "<br>"
@ -359,26 +359,6 @@ export enum RecipientField {
export type FolderInfo = { level: number; folder: MailFolder } 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 const MAX_FOLDER_INDENT_LEVEL = 10
export function getIndentedFolderNameForDropdown(folderInfo: FolderInfo) { 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)) return TUTANOTA_MAIL_ADDRESS_DOMAINS.some((tutaDomain) => mailAddress.endsWith("@" + tutaDomain))
} }
/** export function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mail): boolean {
* 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 {
switch (encryptionAuthStatus) { switch (encryptionAuthStatus) {
// emails before tuta-crypt had no encryptionAuthStatus // emails before tuta-crypt had no encryptionAuthStatus
case null: 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? * 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 type { LoginController } from "../../api/main/LoginController"
import { assertMainOrNode } from "../../api/common/Env" import { assertMainOrNode } from "../../api/common/Env"
import { PartialRecipient } from "../../api/common/recipients/Recipient" import { PartialRecipient } from "../../api/common/recipients/Recipient"

View file

@ -1,15 +1,17 @@
import m from "mithril" import m from "mithril"
import { locator } from "../../api/main/CommonLocator" import { locator } from "../../api/main/CommonLocator"
import { MailSetKind } from "../../api/common/TutanotaConstants.js" import { MailSetKind } from "../../api/common/TutanotaConstants.js"
import { assertSystemFolderOfType } from "../../../mail-app/mail/model/MailModel.js"
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js" import { mailLocator } from "../../../mail-app/mailLocator.js"
import { assertNotNull } from "@tutao/tutanota-utils"
import { getElementId } from "../../api/common/utils/EntityUtils.js" import { getElementId } from "../../api/common/utils/EntityUtils.js"
export async function openMailbox(userId: Id, mailAddress: string, requestedPath: string | null) { export async function openMailbox(userId: Id, mailAddress: string, requestedPath: string | null) {
if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) { if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) {
if (!requestedPath) { if (!requestedPath) {
const [mailboxDetail] = await locator.mailModel.getMailboxDetails() const [mailboxDetail] = await locator.mailboxModel.getMailboxDetails()
const inbox = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX) const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
const inbox = assertSystemFolderOfType(folders, MailSetKind.INBOX)
m.route.set("/mail/" + getElementId(inbox)) m.route.set("/mail/" + getElementId(inbox))
} else { } else {
m.route.set("/mail" + requestedPath) 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 { AttachmentType, getAttachmentType } from "../../gui/AttachmentBubble.js"
import { showRequestPasswordDialog } from "../../misc/passwords/PasswordRequestDialog.js" import { showRequestPasswordDialog } from "../../misc/passwords/PasswordRequestDialog.js"
import { LoginController } from "../../api/main/LoginController.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 { UsageTestController } from "@tutao/tutanota-usagetests"
import { NativeFileApp } from "../common/FileApp.js" import { NativeFileApp } from "../common/FileApp.js"
import { NativePushServiceApp } from "./NativePushServiceApp.js" import { NativePushServiceApp } from "./NativePushServiceApp.js"
@ -18,11 +18,13 @@ import { AppType } from "../../misc/ClientConstants.js"
export class WebCommonNativeFacade implements CommonNativeFacade { export class WebCommonNativeFacade implements CommonNativeFacade {
constructor( constructor(
private readonly logins: LoginController, private readonly logins: LoginController,
private readonly mailModel: MailModel, private readonly mailboxModel: MailboxModel,
private readonly usageTestController: UsageTestController, private readonly usageTestController: UsageTestController,
private readonly fileApp: lazyAsync<NativeFileApp>, private readonly fileApp: lazyAsync<NativeFileApp>,
private readonly pushService: lazyAsync<NativePushServiceApp>, private readonly pushService: lazyAsync<NativePushServiceApp>,
private readonly fileImportHandler: (filesUris: ReadonlyArray<string>) => unknown, 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, 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 { newMailEditorFromTemplate, newMailtoUrlMailEditor } = await import("../../../mail-app/mail/editor/MailEditor.js")
const signatureModule = await import("../../../mail-app/mail/signature/Signature") const signatureModule = await import("../../../mail-app/mail/signature/Signature")
await this.logins.waitForPartialLogin() await this.logins.waitForPartialLogin()
const mailboxDetails = await this.mailModel.getUserMailboxDetails() const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
let editor let editor
try { try {
@ -130,16 +132,6 @@ export class WebCommonNativeFacade implements CommonNativeFacade {
await pushService.reRegister() 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> { async showAlertDialog(translationKey: string): Promise<void> {
const { Dialog } = await import("../../gui/base/Dialog.js") const { Dialog } = await import("../../gui/base/Dialog.js")
return Dialog.message(translationKey as TranslationKey) 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 { MobileFacade } from "../common/generatedipc/MobileFacade.js"
import { styles } from "../../gui/styles" import { styles } from "../../gui/styles"
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js" import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js"
import { MailModel } from "../../mailFunctionality/MailModel.js" import { MailboxModel } from "../../mailFunctionality/MailboxModel.js"
import { TopLevelView } from "../../../TopLevelView.js" import { TopLevelView } from "../../../TopLevelView.js"
import stream from "mithril/stream" 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 { CalendarViewType } from "../../api/common/utils/CommonCalendarUtils.js"
import { getElementId } from "../../api/common/utils/EntityUtils.js"
assertMainOrNode() assertMainOrNode()
@ -27,8 +26,9 @@ export class WebMobileFacade implements MobileFacade {
constructor( constructor(
private readonly connectivityModel: WebsocketConnectivityModel, private readonly connectivityModel: WebsocketConnectivityModel,
private readonly mailModel: MailModel, private readonly mailboxModel: MailboxModel,
private readonly baseViewPrefix: string, private readonly baseViewPrefix: string,
private readonly mailBackNewRoute?: (currentRoute: string) => Promise<string | null>,
) {} ) {}
public getIsAppVisible(): stream<boolean> { public getIsAppVisible(): stream<boolean> {
@ -86,28 +86,13 @@ export class WebMobileFacade implements MobileFacade {
m.route.set(this.baseViewPrefix) m.route.set(this.baseViewPrefix)
return true return true
} else if (viewSlider && viewSlider.isFirstBackgroundColumnFocused()) { } else if (viewSlider && viewSlider.isFirstBackgroundColumnFocused()) {
// If the first background column is focused in mail view (showing a folder), move to inbox. if (currentRoute.startsWith(MAIL_PREFIX) && this.mailBackNewRoute) {
// If in inbox already, quit const newRoute = await this.mailBackNewRoute(currentRoute)
if (m.route.get().startsWith(MAIL_PREFIX)) { if (newRoute) {
const parts = m.route m.route.set(newRoute)
.get() return true
.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
}
} }
} }
return false return false
} else { } else {
return false return false

View file

@ -73,7 +73,7 @@ export class AboutDialog implements Component<AboutDialogAttrs> {
async _sendDeviceLogs(): Promise<void> { async _sendDeviceLogs(): Promise<void> {
const timestamp = new Date() const timestamp = new Date()
const attachments = await getLogAttachments(timestamp) const attachments = await getLogAttachments(timestamp)
const mailboxDetails = await locator.mailModel.getUserMailboxDetails() const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
let { message, type, client } = clientInfoString(timestamp, true) let { message, type, client } = clientInfoString(timestamp, true)
message = message message = message
.split("\n") .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" import { getDefaultSender, getEnabledMailAddressesWithUser, getMailAddressDisplayText, getSenderNameForUser } from "../mailFunctionality/SharedMailUtils.js"
export function sendShareNotificationEmail(sharedGroupInfo: GroupInfo, recipients: Array<PartialRecipient>, texts: GroupSharingTexts) { 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 senderMailAddress = getDefaultSender(locator.logins, mailboxDetails)
const userName = getSenderNameForUser(mailboxDetails, locator.logins.getUserController()) const userName = getSenderNameForUser(mailboxDetails, locator.logins.getUserController())
// Sending notifications as bcc so that invited people don't see each other // 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, allowRelativeLinks: false,
usePlaceholderForInlineImages: false, usePlaceholderForInlineImages: false,
}).html }).html
locator.mailModel.getUserMailboxDetails().then(async (mailboxDetails) => { locator.mailboxModel.getUserMailboxDetails().then(async (mailboxDetails) => {
const sender = getEnabledMailAddressesWithUser(mailboxDetails, locator.logins.getUserController().userGroupInfo).includes(senderMailAddress) const sender = getEnabledMailAddressesWithUser(mailboxDetails, locator.logins.getUserController().userGroupInfo).includes(senderMailAddress)
? senderMailAddress ? senderMailAddress
: getDefaultSender(locator.logins, mailboxDetails) : getDefaultSender(locator.logins, mailboxDetails)
@ -96,7 +96,7 @@ function _sendNotificationEmail(recipients: Recipients, subject: string, body: s
const confirm = () => Promise.resolve(true) const confirm = () => Promise.resolve(true)
const wait = showProgressDialog 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) const model = await locator.sendMailModel(mailboxDetails, mailboxProperties)
await model.initWithTemplate(recipients, subject, sanitizedBody, [], true, sender) await model.initWithTemplate(recipients, subject, sanitizedBody, [], true, sender)
await model.send(MailMethod.NONE, confirm, wait, "tooManyMailsAuto_msg") 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> { export function writeMail(to: PartialRecipient, subject: string = ""): Promise<unknown> {
return locator.mailModel.getUserMailboxDetails().then((mailboxDetails) => { return locator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
return newMailEditorFromTemplate( return newMailEditorFromTemplate(
mailboxDetails, 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 type { Attachment, InitAsResponseArgs, SendMailModel } from "../../../common/mailFunctionality/SendMailModel.js"
import { Dialog } from "../../../common/gui/base/Dialog" import { Dialog } from "../../../common/gui/base/Dialog"
import { InfoLink, lang } from "../../../common/misc/LanguageViewModel" 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 { checkApprovalStatus } from "../../../common/misc/LoginUtils"
import { locator } from "../../../common/api/main/CommonLocator" import { locator } from "../../../common/api/main/CommonLocator"
import { import {
@ -1156,7 +1156,7 @@ export async function newMailEditorFromTemplate(
senderMailAddress?: string, senderMailAddress?: string,
initialChangedState?: boolean, initialChangedState?: boolean,
): Promise<Dialog> { ): Promise<Dialog> {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return locator return locator
.sendMailModel(mailboxDetails, mailboxProperties) .sendMailModel(mailboxDetails, mailboxProperties)
.then((model) => model.initWithTemplate(recipients, subject, bodyText, attachments, confidential, senderMailAddress, initialChangedState)) .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( async function getMailboxDetailsAndProperties(
mailboxDetails: MailboxDetail | null | undefined, mailboxDetails: MailboxDetail | null | undefined,
): Promise<{ mailboxDetails: MailboxDetail; mailboxProperties: MailboxProperties }> { ): Promise<{ mailboxDetails: MailboxDetail; mailboxProperties: MailboxProperties }> {
mailboxDetails = mailboxDetails ?? (await locator.mailModel.getUserMailboxDetails()) mailboxDetails = mailboxDetails ?? (await locator.mailboxModel.getUserMailboxDetails())
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return { mailboxDetails, mailboxProperties } 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 { isDomainName, isRegularExpression } from "../../../common/misc/FormatValidator"
import { assertNotNull, asyncFind, debounce, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils" import { assertNotNull, asyncFind, debounce, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
import { lang } from "../../../common/misc/LanguageViewModel" 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 { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
import type { SelectorItemList } from "../../../common/gui/base/DropDownSelector.js" import type { SelectorItemList } from "../../../common/gui/base/DropDownSelector.js"
import { elementIdPart, isSameId } from "../../../common/api/common/utils/EntityUtils" 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 { LoginController } from "../../../common/api/main/LoginController.js"
import { getMailHeaders } from "../../../common/mailFunctionality/SharedMailUtils.js" import { getMailHeaders } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { throttle } from "@tutao/tutanota-utils/dist/Utils.js" import { throttle } from "@tutao/tutanota-utils/dist/Utils.js"
import { assertSystemFolderOfType } from "./MailModel.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode() assertMainOrNode()
const moveMailDataPerFolder: MoveMailData[] = [] const moveMailDataPerFolder: MoveMailData[] = []
@ -99,14 +101,21 @@ export class InboxRuleHandler {
* @returns true if a rule matches otherwise false * @returns true if a rule matches otherwise false
*/ */
async findAndApplyMatchingRule(mailboxDetail: MailboxDetail, mail: Mail, applyRulesOnServer: boolean): Promise<{ folder: MailFolder; mail: Mail } | null> { 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 return null
} }
const inboxRule = await _findMatchingRule(this.mailFacade, mail, this.logins.getUserController().props.inboxRules) const inboxRule = await _findMatchingRule(this.mailFacade, mail, this.logins.getUserController().props.inboxRules)
if (inboxRule) { if (inboxRule) {
let inboxFolder = assertNotNull(mailboxDetail.folders.getSystemFolderByType(MailSetKind.INBOX)) const folders = mailLocator.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
let targetFolder = mailboxDetail.folders.getFolderById(elementIdPart(inboxRule.targetFolder)) let inboxFolder = assertNotNull(folders.getSystemFolderByType(MailSetKind.INBOX))
let targetFolder = folders.getFolderById(elementIdPart(inboxRule.targetFolder))
if (targetFolder && targetFolder.folderType !== MailSetKind.INBOX) { if (targetFolder && targetFolder.folderType !== MailSetKind.INBOX) {
if (applyRulesOnServer) { if (applyRulesOnServer) {
@ -225,6 +234,7 @@ function _checkEmailAddresses(mailAddresses: string[], inboxRule: InboxRule): bo
} }
export function isInboxFolder(mailboxDetail: MailboxDetail, mail: Mail): boolean { 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 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 { import {
createMailAddressProperties,
createMailboxProperties,
Mail, Mail,
MailBox,
MailboxGroupRoot, MailboxGroupRoot,
MailboxGroupRootTypeRef,
MailboxProperties, MailboxProperties,
MailboxPropertiesTypeRef,
MailBoxTypeRef,
MailFolder, MailFolder,
MailFolderTypeRef, MailFolderTypeRef,
MailSetEntryTypeRef, MailSetEntryTypeRef,
MailTypeRef, MailTypeRef,
} from "../api/entities/tutanota/TypeRefs.js" } from "../../../common/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"
import { import {
FeatureType, FeatureType,
MailReportType, MailReportType,
@ -32,138 +19,120 @@ import {
MAX_NBR_MOVE_DELETE_MAIL_SERVICE, MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
OperationType, OperationType,
ReportMovedMailsType, ReportMovedMailsType,
} from "../api/common/TutanotaConstants.js" } from "../../../common/api/common/TutanotaConstants.js"
import { assertSystemFolderOfType, getEnabledMailAddressesWithUser } from "./SharedMailUtils.js" import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../../../common/api/common/utils/EntityUtils.js"
import { LockedError, NotFoundError, PreconditionFailedError } from "../api/common/error/RestError.js" import { FolderInfo } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../api/common/utils/EntityUtils.js" import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
import m from "mithril" import m from "mithril"
import { lang } from "../misc/LanguageViewModel.js" import { WebsocketCounterData } from "../../../common/api/entities/sys/TypeRefs.js"
import { ProgrammingError } from "../api/common/error/ProgrammingError.js" import { Notifications, NotificationType } from "../../../common/gui/Notifications.js"
import { UserError } from "../api/main/UserError.js" import { lang } from "../../../common/misc/LanguageViewModel.js"
import { isSpamOrTrashFolder } from "../api/common/CommonMailUtils.js" import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
import { LockedError, NotFoundError, PreconditionFailedError } from "../../../common/api/common/error/RestError.js"
export type MailboxDetail = { import { UserError } from "../../../common/api/main/UserError.js"
mailbox: MailBox import { EventController } from "../../../common/api/main/EventController.js"
folders: FolderSystem import { InboxRuleHandler } from "./InboxRuleHandler.js"
mailGroupInfo: GroupInfo import { WebsocketConnectivityModel } from "../../../common/misc/WebsocketConnectivityModel.js"
mailGroup: Group import { EntityClient } from "../../../common/api/common/EntityClient.js"
mailboxGroupRoot: MailboxGroupRoot import { LoginController } from "../../../common/api/main/LoginController.js"
} import { MailFacade } from "../../../common/api/worker/facades/lazy/MailFacade.js"
import { mailLocator } from "../../mailLocator.js"
export type MailboxCounters = Record<Id, Record<string, number>>
export class MailModel { 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({}) readonly mailboxCounters: Stream<MailboxCounters> = stream({})
private initialization: Promise<void> | null = null readonly folders: Stream<Record<Id, FolderSystem>> = stream()
/**
* 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( constructor(
private readonly notifications: Notifications, private readonly notifications: Notifications,
private readonly mailboxModel: MailboxModel,
private readonly eventController: EventController, private readonly eventController: EventController,
private readonly mailFacade: MailFacade,
private readonly entityClient: EntityClient, private readonly entityClient: EntityClient,
private readonly logins: LoginController, private readonly logins: LoginController,
private readonly mailFacade: MailFacade,
private readonly connectivityModel: WebsocketConnectivityModel | null, private readonly connectivityModel: WebsocketConnectivityModel | null,
private readonly inboxRuleHandler: InboxRuleHandler | null, private readonly inboxRuleHandler: InboxRuleHandler | null,
) {} ) {}
// only init listeners once // only init listeners once
private readonly initListeners = lazyMemoized(() => { 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.eventController.getCountersStream().map((update) => {
this._mailboxCountersUpdates(update) this._mailboxCountersUpdates(update)
}) })
}) })
init(): Promise<void> { async 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() this.initListeners()
return this._init() const mailboxDetails = this.mailboxModel.mailboxDetails() || []
}
private _init(): Promise<void> { let tempFolders: Record<Id, FolderSystem> = {}
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
})
}
/** for (let detail of mailboxDetails) {
* load mailbox details from a mailgroup membership if (detail.mailbox.folders) {
*/ const detailFolders = await this.mailboxModel.loadFolders(neverNull(detail.mailbox.folders).folders)
private async mailboxDetailsFromMembership(membership: GroupMembership): Promise<MailboxDetail> { tempFolders[detail.mailbox.folders._id] = new FolderSystem(detailFolders)
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,
} }
this.folders(tempFolders)
} }
private loadFolders(folderListId: Id): Promise<MailFolder[]> { async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>): Promise<void> {
return this.entityClient.loadAll(MailFolderTypeRef, folderListId).then((folders) => { for (const update of updates) {
return folders.filter((f) => { if (isUpdateForTypeRef(MailFolderTypeRef, update)) {
// We do not show spam or archive for external users await this.init()
if (!this.logins.isInternalUserLoggedIn() && (f.folderType === MailSetKind.SPAM || f.folderType === MailSetKind.ARCHIVE)) { m.redraw()
return false } else if (
} else { isUpdateForTypeRef(MailTypeRef, update) &&
return !(this.logins.isEnabled(FeatureType.InternalCommunication) && f.folderType === MailSetKind.SPAM) 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> { 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 const detail = mailboxDetails.find((md) => md.folders.getFolderByMail(mail)) ?? null
if (detail == null) { if (detail == null) {
console.warn("Mailbox detail for mail does not exist", mail) console.warn("Mailbox detail for mail does not exist", mail)
@ -172,7 +141,7 @@ export class MailModel {
} }
async getMailboxDetailsForMailFolder(mailFolder: MailFolder): Promise<MailboxDetail | null> { 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 const detail = mailboxDetails.find((md) => md.folders.getFolderById(getElementId(mailFolder))) ?? null
if (detail == null) { if (detail == null) {
console.warn("Mailbox detail for mail folder does not exist", mailFolder) console.warn("Mailbox detail for mail folder does not exist", mailFolder)
@ -180,29 +149,23 @@ export class MailModel {
return detail return detail
} }
async getMailboxDetailsForMailGroup(mailGroupId: Id): Promise<MailboxDetail> { getMailboxFoldersForMail(mail: Mail): Promise<FolderSystem | null> {
const mailboxDetails = await this.getMailboxDetails() return this.getMailboxDetailsForMail(mail).then((md) => {
return assertNotNull( if (md && md.mailbox.folders) {
mailboxDetails.find((md) => mailGroupId === md.mailGroup._id), const folderStructures = this.folders()
"Mailbox detail for mail group does not exist", return folderStructures[md.mailbox.folders._id] ?? null
) }
return null
})
} }
async getUserMailboxDetails(): Promise<MailboxDetail> { getMailboxFoldersForId(foldersId: Id): FolderSystem {
const userMailGroupMembership = this.logins.getUserController().getUserMailGroupMembership() const folderStructures = this.folders()
const mailboxDetails = await this.getMailboxDetails() return folderStructures[foldersId]
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)
} }
getMailFolderForMail(mail: Mail): MailFolder | null { getMailFolderForMail(mail: Mail): MailFolder | null {
const mailboxDetails = this.mailboxDetails() || [] const mailboxDetails = this.mailboxModel.mailboxDetails() || []
let foundFolder: MailFolder | null = null let foundFolder: MailFolder | null = null
for (let detail of mailboxDetails) { for (let detail of mailboxDetails) {
@ -217,27 +180,6 @@ export class MailModel {
return null 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 * Finally move all given mails. Caller must ensure that mails are only from
* * one folder (because we send one source folder) * * 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, * Finally deletes the given mails if they are already in the trash or spam folders,
* otherwise moves them to the trash folder. * 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) { * Finally deletes all given mails. Caller must ensure that mails are only from one folder and the folder must allow final delete operation.
if (isUpdateForTypeRef(MailFolderTypeRef, update)) { */
await this._init() private async finallyDeleteMails(mails: Mail[]): Promise<void> {
m.redraw() if (!mails.length) return Promise.resolve()
} else if (isUpdateForTypeRef(GroupInfoTypeRef, update)) { const mailFolder = neverNull(this.getMailFolderForMail(mails[0]))
if (update.operation === OperationType.UPDATE) { const mailIds = mails.map((m) => m._id)
await this._init() const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, mailIds)
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) { for (const mailChunk of mailChunks) {
await this._init() await this.mailFacade.deleteMails(mailChunk, mailFolder._id)
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
}
}
}
}
} }
} }
/**
* 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) { _mailboxCountersUpdates(counters: WebsocketCounterData) {
const normalized = this.mailboxCounters() || {} const normalized = this.mailboxCounters() || {}
const group = normalized[counters.mailGroup] || {} const group = normalized[counters.mailGroup] || {}
@ -562,77 +474,80 @@ export class MailModel {
await this.mailFacade.unsubscribe(mail._id, recipient, headers) 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> { async saveReportMovedMails(mailboxGroupRoot: MailboxGroupRoot, reportMovedMails: ReportMovedMailsType): Promise<MailboxProperties> {
const mailboxProperties = await this.loadOrCreateMailboxProperties(mailboxGroupRoot) const mailboxProperties = await this.mailboxModel.loadOrCreateMailboxProperties(mailboxGroupRoot)
mailboxProperties.reportMovedMails = reportMovedMails mailboxProperties.reportMovedMails = reportMovedMails
await this.entityClient.update(mailboxProperties) await this.entityClient.update(mailboxProperties)
return 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 { isMailAddress } from "../../../common/misc/FormatValidator"
import { UserError } from "../../../common/api/main/UserError" import { UserError } from "../../../common/api/main/UserError"
import { showUserError } from "../../../common/misc/ErrorHandlerImpl" 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 { Keys, MailMethod, TabIndex } from "../../../common/api/common/TutanotaConstants"
import { progressIcon } from "../../../common/gui/base/Icon" import { progressIcon } from "../../../common/gui/base/Icon"
import { Editor } from "../../../common/gui/editor/Editor" import { Editor } from "../../../common/gui/editor/Editor"
@ -29,7 +29,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
} }
async function send() { async function send() {
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const body = pressRelease.bodyHtml() const body = pressRelease.bodyHtml()
const subject = pressRelease.subject() const subject = pressRelease.subject()
let recipients let recipients
@ -112,7 +112,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
const bodyWithGreeting = `<p>${recipient.greeting},</p>${body}` const bodyWithGreeting = `<p>${recipient.greeting},</p>${body}`
try { 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 sendMailModel = await locator.sendMailModel(mailboxDetails, mailboxProperties)
const model = await sendMailModel.initWithTemplate( 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 { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
import { ConversationType, MailSetKind, MailState, OperationType } from "../../../common/api/common/TutanotaConstants.js" import { ConversationType, MailSetKind, MailState, OperationType } from "../../../common/api/common/TutanotaConstants.js"
import { NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError.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 { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.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 export type MailViewerViewModelFactory = (options: CreateMailViewerOptions) => MailViewerViewModel
@ -257,7 +258,11 @@ export class ConversationViewModel {
private async isInTrash(mail: Mail) { private async isInTrash(mail: Mail) {
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(mail) const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(mail)
const mailFolder = this.mailModel.getMailFolderForMail(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> { 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 { locator } from "../../../common/api/main/CommonLocator.js"
import { LockedError } from "../../../common/api/common/error/RestError.js" import { LockedError } from "../../../common/api/common/error/RestError.js"
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel.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 { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
import { elementIdPart, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js" import { elementIdPart, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js"
import { reportMailsAutomatically } from "./MailReportDialog.js" import { reportMailsAutomatically } from "./MailReportDialog.js"
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js" import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
import { getFolderName, getIndentedFolderNameForDropdown, getPathToFolderString } from "../../../common/mailFunctionality/SharedMailUtils.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 { 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. * 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) { export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedFolder: MailFolder | null = null, parentFolder: MailFolder | null = null) {
const noParentFolderOption = lang.get("comboBoxSelectionNone_msg") const noParentFolderOption = lang.get("comboBoxSelectionNone_msg")
const mailGroupId = mailBoxDetail.mailGroup._id const mailGroupId = mailBoxDetail.mailGroup._id
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailBoxDetail.mailbox.folders)._id)
let folderNameValue = editedFolder?.name ?? "" let folderNameValue = editedFolder?.name ?? ""
let targetFolders: SelectorItemList<MailFolder | null> = mailBoxDetail.folders let targetFolders: SelectorItemList<MailFolder | null> = folders
.getIndentedList(editedFolder) .getIndentedList(editedFolder)
// filter: SPAM and TRASH and descendants are only shown if editing (folders can only be moved there, not created there) // 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))) .filter((folderInfo: IndentedFolder) => !(editedFolder === null && isSpamOrTrashFolder(folders, folderInfo.folder)))
.map((folderInfo) => { .map((folderInfo: IndentedFolder) => {
return { return {
name: getIndentedFolderNameForDropdown(folderInfo), name: getIndentedFolderNameForDropdown(folderInfo),
value: folderInfo.folder, value: folderInfo.folder,
@ -49,7 +53,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
selectedValue: selectedParentFolder, selectedValue: selectedParentFolder,
selectedValueDisplay: selectedParentFolder ? getFolderName(selectedParentFolder) : noParentFolderOption, selectedValueDisplay: selectedParentFolder ? getFolderName(selectedParentFolder) : noParentFolderOption,
selectionChangedHandler: (newFolder: MailFolder | null) => (selectedParentFolder = newFolder), 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 if (!confirmed) return
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue) 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)) { } 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 // if it is being moved to spam (and not already in spam), ask about reporting containing emails
const confirmed = await Dialog.confirm(() => const confirmed = await Dialog.confirm(() =>
@ -102,16 +106,16 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
if (!confirmed) return if (!confirmed) return
// get mails to report before moving to mail model // 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> = [] let reportableMails: Array<Mail> = []
await loadAllMailsOfFolder(editedFolder, reportableMails) await loadAllMailsOfFolder(editedFolder, reportableMails)
for (const descendant of descendants) { for (const descendant of descendants) {
await loadAllMailsOfFolder(descendant.folder, reportableMails) 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.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
await locator.mailModel.sendFolderToSpam(editedFolder) await mailLocator.mailModel.sendFolderToSpam(editedFolder)
} else { } else {
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue) await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
await locator.mailFacade.updateMailFolderParent(editedFolder, selectedParentFolder?._id || null) await locator.mailFacade.updateMailFolderParent(editedFolder, selectedParentFolder?._id || null)
@ -127,16 +131,22 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
Dialog.showActionDialog({ Dialog.showActionDialog({
title: editedFolder ? lang.get("editFolder_action") : lang.get("addFolder_action"), title: editedFolder ? lang.get("editFolder_action") : lang.get("addFolder_action"),
child: form, child: form,
validator: () => checkFolderName(mailBoxDetail, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null), validator: () => checkFolderName(mailBoxDetail, folders, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null),
allowOkWithReturn: true, allowOkWithReturn: true,
okAction: okAction, 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() === "") { if (name.trim() === "") {
return "folderNameNeutral_msg" 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" return "folderNameInvalidExisting_msg"
} else { } else {
return null return null

View file

@ -100,7 +100,10 @@ export function sendResponse(event: CalendarEvent, recipient: string, status: Ca
return 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) { if (replyResult === ReplyResult.ReplySent) {
ownAttendee.status = status ownAttendee.status = status
} }

View file

@ -1,14 +1,14 @@
import m, { Child, Children, Component, Vnode } from "mithril" 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 { locator } from "../../../common/api/main/CommonLocator.js"
import { SidebarSection } from "../../../common/gui/SidebarSection.js" import { SidebarSection } from "../../../common/gui/SidebarSection.js"
import { IconButton, IconButtonAttrs } from "../../../common/gui/base/IconButton.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 { elementIdPart, getElementId } from "../../../common/api/common/utils/EntityUtils.js"
import { isSelectedPrefix, NavButtonAttrs, NavButtonColor } from "../../../common/gui/base/NavButton.js" import { isSelectedPrefix, NavButtonAttrs, NavButtonColor } from "../../../common/gui/base/NavButton.js"
import { MAIL_PREFIX } from "../../../common/misc/RouteChange.js" import { MAIL_PREFIX } from "../../../common/misc/RouteChange.js"
import { MailFolderRow } from "./MailFolderRow.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 { MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { attachDropdown, DropdownButtonAttrs } from "../../../common/gui/base/Dropdown.js" import { attachDropdown, DropdownButtonAttrs } from "../../../common/gui/base/Dropdown.js"
import { Icons } from "../../../common/gui/base/icons/Icons.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 { px, size } from "../../../common/gui/size.js"
import { RowButton } from "../../../common/gui/base/buttons/RowButton.js" import { RowButton } from "../../../common/gui/base/buttons/RowButton.js"
import { getFolderIcon, getFolderName, MAX_FOLDER_INDENT_LEVEL } from "../../../common/mailFunctionality/SharedMailUtils.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 { export interface MailFolderViewAttrs {
mailModel: MailModel
mailboxDetail: MailboxDetail mailboxDetail: MailboxDetail
mailFolderElementIdToSelectedMailId: ReadonlyMap<Id, Id> mailFolderElementIdToSelectedMailId: ReadonlyMap<Id, Id>
onFolderClick: (folder: MailFolder) => unknown onFolderClick: (folder: MailFolder) => unknown
@ -41,20 +43,21 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
private visibleRow: string | null = null private visibleRow: string | null = null
view({ attrs }: Vnode<MailFolderViewAttrs>): Children { view({ attrs }: Vnode<MailFolderViewAttrs>): Children {
const { mailboxDetail } = attrs const { mailboxDetail, mailModel } = attrs
const groupCounters = locator.mailModel.mailboxCounters()[mailboxDetail.mailGroup._id] || {} 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 // 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 // So instead we push or not push into array
const customSystems = mailboxDetail.folders.customSubtrees const customSystems = folders.customSubtrees
const systemSystems = mailboxDetail.folders.systemSubtrees const systemSystems = folders.systemSubtrees
const children: Children = [] const children: Children = []
const selectedFolder = mailboxDetail.folders const selectedFolder = folders
.getIndentedList() .getIndentedList()
.map((f) => f.folder) .map((f) => f.folder)
.find((f) => isSelectedPrefix(MAIL_PREFIX + "/" + getElementId(f))) .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 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) { if (systemChildren) {
children.push(...systemChildren.children) children.push(...systemChildren.children)
} }
@ -67,7 +70,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
button: attrs.inEditMode ? this.renderCreateFolderAddButton(null, attrs) : this.renderEditFoldersButton(attrs), 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. 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)) children.push(this.renderAddFolderButtonRow(attrs))
@ -78,6 +81,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
private renderFolderTree( private renderFolderTree(
subSystems: readonly FolderSubtree[], subSystems: readonly FolderSubtree[],
groupCounters: Counters, groupCounters: Counters,
folders: FolderSystem,
attrs: MailFolderViewAttrs, attrs: MailFolderViewAttrs,
path: MailFolder[], path: MailFolder[],
isInternalUser: boolean, isInternalUser: boolean,
@ -115,13 +119,13 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
const summedCount = !currentExpansionState && hasChildren ? this.getTotalFolderCounter(groupCounters, system) : groupCounters[counterId] const summedCount = !currentExpansionState && hasChildren ? this.getTotalFolderCounter(groupCounters, system) : groupCounters[counterId]
const childResult = const childResult =
hasChildren && currentExpansionState 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 } : { children: null, numRows: 0 }
const isTrashOrSpam = system.folder.folderType === MailSetKind.TRASH || system.folder.folderType === MailSetKind.SPAM const isTrashOrSpam = system.folder.folderType === MailSetKind.TRASH || system.folder.folderType === MailSetKind.SPAM
const isRightButtonVisible = this.visibleRow === id const isRightButtonVisible = this.visibleRow === id
const rightButton = const rightButton =
isInternalUser && !isTrashOrSpam && (isRightButtonVisible || attrs.inEditMode) isInternalUser && !isTrashOrSpam && (isRightButtonVisible || attrs.inEditMode)
? this.createFolderMoreButton(system.folder, attrs, () => { ? this.createFolderMoreButton(system.folder, folders, attrs, () => {
this.visibleRow = null this.visibleRow = null
}) })
: 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) 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({ return attachDropdown({
mainButtonAttrs: { mainButtonAttrs: {
title: "more_label", title: "more_label",
@ -188,9 +192,9 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
childAttrs: () => { childAttrs: () => {
return folder.folderType === MailSetKind.CUSTOM return folder.folderType === MailSetKind.CUSTOM
? // cannot add new folder to custom folder in spam or trash folder ? // cannot add new folder to custom folder in spam or trash folder
isSpamOrTrashFolder(attrs.mailboxDetail.folders, folder) isSpamOrTrashFolder(folders, folder)
? [this.editButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)] ? [this.editButtonAttrs(attrs, folders, folder), this.deleteButtonAttrs(attrs, folder)]
: [this.editButtonAttrs(attrs, folder), this.addButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)] : [this.editButtonAttrs(attrs, folders, folder), this.addButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
: [this.addButtonAttrs(attrs, folder)] : [this.addButtonAttrs(attrs, folder)]
}, },
onClose, 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 { return {
label: "edit_action", label: "edit_action",
icon: Icons.Edit, icon: Icons.Edit,
@ -225,7 +229,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
attrs.onShowFolderAddEditDialog( attrs.onShowFolderAddEditDialog(
attrs.mailboxDetail.mailGroup._id, attrs.mailboxDetail.mailGroup._id,
folder, 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 { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import type { File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js" import { createMail, File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { createMail } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError" import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
import { Dialog } from "../../../common/gui/base/Dialog" import { Dialog } from "../../../common/gui/base/Dialog"
import { locator } from "../../../common/api/main/CommonLocator" import { locator } from "../../../common/api/main/CommonLocator"
import { AllIcons } from "../../../common/gui/base/Icon" import { AllIcons } from "../../../common/gui/base/Icon"
import { Icons } from "../../../common/gui/base/icons/Icons" import { Icons } from "../../../common/gui/base/icons/Icons"
import { isApp, isDesktop } from "../../../common/api/common/Env" import { isApp, isDesktop } from "../../../common/api/common/Env"
import { assertNotNull, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils" import { assertNotNull, endsWith, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants" 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 { reportMailsAutomatically } from "./MailReportDialog"
import { DataFile } from "../../../common/api/common/DataFile" import { DataFile } from "../../../common/api/common/DataFile"
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel" 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 { PinchZoom } from "../../../common/gui/PinchZoom.js"
import { InlineImageReference, InlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js" import { InlineImageReference, InlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
import { import {
assertSystemFolderOfType,
getFolderIcon, getFolderIcon,
getFolderName, getFolderName,
getIndentedFolderNameForDropdown, getIndentedFolderNameForDropdown,
getMoveTargetFolderSystems, hasValidEncryptionAuthForTeamOrSystemMail,
isOfTypeOrSubfolderOf,
} from "../../../common/mailFunctionality/SharedMailUtils.js" } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js" import { assertSystemFolderOfType, getMoveTargetFolderSystems, isOfTypeOrSubfolderOf, isSpamOrTrashFolder, MailModel } from "../model/MailModel.js"
import { getElementId } from "../../../common/api/common/utils/EntityUtils.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> { export async function showDeleteConfirmationDialog(mails: ReadonlyArray<Mail>): Promise<boolean> {
let trashMails: Mail[] = [] let trashMails: Mail[] = []
let moveMails: Mail[] = [] let moveMails: Mail[] = []
for (let mail of mails) { for (let mail of mails) {
const folder = locator.mailModel.getMailFolderForMail(mail) const folder = mailLocator.mailModel.getMailFolderForMail(mail)
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMail(mail) const folders = await mailLocator.mailModel.getMailboxFoldersForMail(mail)
if (mailboxDetail == null) { if (folders == null) {
continue continue
} }
const isFinalDelete = folder && isSpamOrTrashFolder(mailboxDetail.folders, folder) const isFinalDelete = folder && isSpamOrTrashFolder(folders, folder)
isFinalDelete ? trashMails.push(mail) : moveMails.push(mail) isFinalDelete ? trashMails.push(mail) : moveMails.push(mail)
} }
@ -85,6 +84,7 @@ export function promptAndDeleteMails(mailModel: MailModel, mails: ReadonlyArray<
} }
interface MoveMailsParams { interface MoveMailsParams {
mailboxModel: MailboxModel
mailModel: MailModel mailModel: MailModel
mails: ReadonlyArray<Mail> mails: ReadonlyArray<Mail>
targetMailFolder: MailFolder targetMailFolder: MailFolder
@ -95,12 +95,12 @@ interface MoveMailsParams {
* Moves the mails and reports them as spam if the user or settings allow it. * Moves the mails and reports them as spam if the user or settings allow it.
* @return whether mails were actually moved * @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) const details = await mailModel.getMailboxDetailsForMailFolder(targetMailFolder)
if (details == null) { if (details == null || details.mailbox.folders == null) {
return false return false
} }
const system = details.folders const system = mailModel.getMailboxFoldersForId(details.mailbox.folders._id)
return mailModel return mailModel
.moveMails(mails, targetMailFolder) .moveMails(mails, targetMailFolder)
.then(async () => { .then(async () => {
@ -111,8 +111,8 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
reportableMail._id = targetMailFolder.isMailSet ? mail._id : [targetMailFolder.mails, getElementId(mail)] reportableMail._id = targetMailFolder.isMailSet ? mail._id : [targetMailFolder.mails, getElementId(mail)]
return reportableMail return reportableMail
}) })
const mailboxDetails = await mailModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup)) const mailboxDetails = await mailboxModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
await reportMailsAutomatically(MailReportType.SPAM, mailModel, mailboxDetails, reportableMails) await reportMailsAutomatically(MailReportType.SPAM, mailboxModel, mailModel, mailboxDetails, reportableMails)
} }
return true return true
@ -130,10 +130,11 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
export function archiveMails(mails: Mail[]): Promise<void> { export function archiveMails(mails: Mail[]): Promise<void> {
if (mails.length > 0) { if (mails.length > 0) {
// assume all mails in the array belong to the same Mailbox // 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 && folders &&
moveMails({ moveMails({
mailModel: locator.mailModel, mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: mails, mails: mails,
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.ARCHIVE), targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.ARCHIVE),
}) })
@ -146,10 +147,11 @@ export function archiveMails(mails: Mail[]): Promise<void> {
export function moveToInbox(mails: Mail[]): Promise<any> { export function moveToInbox(mails: Mail[]): Promise<any> {
if (mails.length > 0) { if (mails.length > 0) {
// assume all mails in the array belong to the same Mailbox // 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 && folders &&
moveMails({ moveMails({
mailModel: locator.mailModel, mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: mails, mails: mails,
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.INBOX), targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.INBOX),
}) })
@ -160,7 +162,7 @@ export function moveToInbox(mails: Mail[]): Promise<any> {
} }
export function getMailFolderIcon(mail: Mail): AllIcons { export function getMailFolderIcon(mail: Mail): AllIcons {
let folder = locator.mailModel.getMailFolderForMail(mail) let folder = mailLocator.mailModel.getMailFolderForMail(mail)
if (folder) { if (folder) {
return getFolderIcon(folder) return getFolderIcon(folder)
@ -291,6 +293,7 @@ export function getReferencedAttachments(attachments: Array<TutanotaFile>, refer
} }
export async function showMoveMailsDropdown( export async function showMoveMailsDropdown(
mailboxModel: MailboxModel,
model: MailModel, model: MailModel,
origin: PosRect, origin: PosRect,
mails: readonly Mail[], mails: readonly Mail[],
@ -307,7 +310,7 @@ export async function showMoveMailsDropdown(
text: () => getIndentedFolderNameForDropdown(f), text: () => getIndentedFolderNameForDropdown(f),
click: () => { click: () => {
onSelected() onSelected()
moveMails({ mailModel: model, mails: mails, targetMailFolder: f.folder }) moveMails({ mailboxModel, mailModel: model, mails: mails, targetMailFolder: f.folder })
}, },
icon: getFolderIcon(f.folder), icon: getFolderIcon(f.folder),
} satisfies DropdownChildAttrs), } 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 // 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) 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 { theme } from "../../../common/gui/theme.js"
import { VirtualRow } from "../../../common/gui/base/ListUtils.js" import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
import { isKeyPressed } from "../../../common/misc/KeyManager.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 { 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() assertMainOrNode()
@ -402,14 +404,16 @@ export class MailListView implements Component<MailListViewAttrs> {
const selectedFolder = this.mailViewModel.getFolder() const selectedFolder = this.mailViewModel.getFolder()
if (selectedFolder) { if (selectedFolder) {
const mailDetails = await this.mailViewModel.getMailboxDetails() const mailDetails = await this.mailViewModel.getMailboxDetails()
return isOfTypeOrSubfolderOf(mailDetails.folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH if (mailDetails.mailbox.folders) {
} else { const folders = mailLocator.mailModel.getMailboxFoldersForId(mailDetails.mailbox.folders._id)
return false return isOfTypeOrSubfolderOf(folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH
}
} }
return false
} }
private async onSwipeLeft(listElement: Mail): Promise<ListSwipeDecision> { 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 return wereDeleted ? ListSwipeDecision.Commit : ListSwipeDecision.Cancel
} }
@ -419,7 +423,7 @@ export class MailListView implements Component<MailListViewAttrs> {
this.mailViewModel.listModel?.selectNone() this.mailViewModel.listModel?.selectNone()
return ListSwipeDecision.Cancel return ListSwipeDecision.Cancel
} else { } else {
const folders = await locator.mailModel.getMailboxFolders(listElement) const folders = await mailLocator.mailModel.getMailboxFoldersForMail(listElement)
if (folders) { if (folders) {
//Check if the user is in the trash/spam folder or if it's in Inbox or Archive //Check if the user is in the trash/spam folder or if it's in Inbox or Archive
//to determinate the target folder //to determinate the target folder
@ -427,7 +431,8 @@ export class MailListView implements Component<MailListViewAttrs> {
? this.getRecoverFolder(listElement, folders) ? this.getRecoverFolder(listElement, folders)
: assertNotNull(folders.getSystemFolderByType(this.showingArchive ? MailSetKind.INBOX : MailSetKind.ARCHIVE)) : assertNotNull(folders.getSystemFolderByType(this.showingArchive ? MailSetKind.INBOX : MailSetKind.ARCHIVE))
const wereMoved = await moveMails({ const wereMoved = await moveMails({
mailModel: locator.mailModel, mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: [listElement], mails: [listElement],
targetMailFolder, targetMailFolder,
}) })

View file

@ -5,8 +5,9 @@ import m from "mithril"
import { MailReportType, ReportMovedMailsType } from "../../../common/api/common/TutanotaConstants" import { MailReportType, ReportMovedMailsType } from "../../../common/api/common/TutanotaConstants"
import { ButtonAttrs, ButtonType } from "../../../common/gui/base/Button.js" import { ButtonAttrs, ButtonType } from "../../../common/gui/base/Button.js"
import { Dialog } from "../../../common/gui/base/Dialog" 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 { showSnackBar } from "../../../common/gui/base/SnackBar"
import { MailModel } from "../model/MailModel.js"
function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDetail): Promise<boolean> { function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDetail): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -60,6 +61,7 @@ function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDe
*/ */
export async function reportMailsAutomatically( export async function reportMailsAutomatically(
mailReportType: MailReportType, mailReportType: MailReportType,
mailboxModel: MailboxModel,
mailModel: MailModel, mailModel: MailModel,
mailboxDetails: MailboxDetail, mailboxDetails: MailboxDetail,
mails: ReadonlyArray<Mail>, mails: ReadonlyArray<Mail>,
@ -68,7 +70,7 @@ export async function reportMailsAutomatically(
return 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 allowUndoing = true // decides if a snackbar is shown to prevent the server request
let isReportable = false 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 { NBSP, noOp } from "@tutao/tutanota-utils"
import { VirtualRow } from "../../../common/gui/base/ListUtils.js" import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
import { companyTeamLabel } from "../../../common/misc/ClientConstants.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> = { const iconMap: Record<MailSetKind, string> = {
[MailSetKind.CUSTOM]: FontIcons.Folder, [MailSetKind.CUSTOM]: FontIcons.Folder,
@ -255,7 +257,7 @@ export class MailRow implements VirtualRow<Mail> {
let iconText = "" let iconText = ""
if (this.showFolderIcon) { if (this.showFolderIcon) {
let folder = locator.mailModel.getMailFolderForMail(mail) let folder = mailLocator.mailModel.getMailFolderForMail(mail)
iconText += folder ? this.folderIcon(getMailFolderType(folder)) : "" 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 { getMailSelectionMessage, MultiItemViewer } from "./MultiItemViewer.js"
import { Icons } from "../../../common/gui/base/icons/Icons" import { Icons } from "../../../common/gui/base/icons/Icons"
import { showProgressDialog } from "../../../common/gui/dialogs/ProgressDialog" 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 { locator } from "../../../common/api/main/CommonLocator"
import { PermissionError } from "../../../common/api/common/error/PermissionError" import { PermissionError } from "../../../common/api/common/error/PermissionError"
import { styles } from "../../../common/gui/styles" import { styles } from "../../../common/gui/styles"
@ -58,8 +58,9 @@ import { MailFilterButton } from "./MailFilterButton.js"
import { listSelectionKeyboardShortcuts } from "../../../common/gui/base/ListUtils.js" import { listSelectionKeyboardShortcuts } from "../../../common/gui/base/ListUtils.js"
import { canDoDragAndDropExport, getFolderName, getMailboxName } from "../../../common/mailFunctionality/SharedMailUtils.js" import { canDoDragAndDropExport, getFolderName, getMailboxName } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { BottomNav } from "../../gui/BottomNav.js" import { BottomNav } from "../../gui/BottomNav.js"
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
import { showSnackBar } from "../../../common/gui/base/SnackBar.js" import { showSnackBar } from "../../../common/gui/base/SnackBar.js"
import { isSpamOrTrashFolder } from "../model/MailModel.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode() assertMainOrNode()
@ -227,7 +228,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
vnode.attrs.mailViewModel.init() vnode.attrs.mailViewModel.init()
this.oncreate = () => { this.oncreate = () => {
this.countersStream = locator.mailModel.mailboxCounters.map(m.redraw) this.countersStream = mailLocator.mailModel.mailboxCounters.map(m.redraw)
keyManager.registerShortcuts(shortcuts) keyManager.registerShortcuts(shortcuts)
this.cache.conversationViewPreference = deviceConfig.getConversationViewShowOnlySelectedMail() this.cache.conversationViewPreference = deviceConfig.getConversationViewShowOnlySelectedMail()
} }
@ -249,6 +250,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
private mailViewerSingleActions(viewModel: ConversationViewModel) { private mailViewerSingleActions(viewModel: ConversationViewModel) {
return m(MailViewerActions, { return m(MailViewerActions, {
mailboxModel: viewModel.primaryViewModel().mailboxModel,
mailModel: viewModel.primaryViewModel().mailModel, mailModel: viewModel.primaryViewModel().mailModel,
mailViewerViewModel: viewModel.primaryViewModel(), mailViewerViewModel: viewModel.primaryViewModel(),
mails: [viewModel.primaryMail], mails: [viewModel.primaryMail],
@ -283,7 +285,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
private mailViewerMultiActions() { private mailViewerMultiActions() {
return m(MailViewerActions, { return m(MailViewerActions, {
mailModel: locator.mailModel, mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: this.mailViewModel.listModel?.getSelectedAsArray() ?? [], mails: this.mailViewModel.listModel?.getSelectedAsArray() ?? [],
selectNone: () => this.mailViewModel.listModel?.selectNone(), selectNone: () => this.mailViewModel.listModel?.selectNone(),
}) })
@ -375,7 +378,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
? m(MobileMailMultiselectionActionBar, { ? m(MobileMailMultiselectionActionBar, {
mails: this.mailViewModel.listModel.getSelectedAsArray(), mails: this.mailViewModel.listModel.getSelectedAsArray(),
selectNone: () => this.mailViewModel.listModel?.selectNone(), selectNone: () => this.mailViewModel.listModel?.selectNone(),
mailModel: locator.mailModel, mailModel: mailLocator.mailModel,
mailboxModel: locator.mailboxModel,
}) })
: m(BottomNav), : m(BottomNav),
}), }),
@ -545,7 +549,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
const selectedMails = mailList.getSelectedAsArray() const selectedMails = mailList.getSelectedAsArray()
showMoveMailsDropdown(locator.mailModel, getMoveMailBounds(), selectedMails) showMoveMailsDropdown(locator.mailboxModel, mailLocator.mailModel, getMoveMailBounds(), selectedMails)
} }
private createFolderColumn(editingFolderForMailGroup: Id | null = null, drawerAttrs: DrawerMenuAttrs) { 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) { private renderFolders(editingFolderForMailGroup: Id | null) {
const details = locator.mailModel.mailboxDetails() ?? [] const details = locator.mailboxModel.mailboxDetails() ?? []
return [ return [
...details.map((mailboxDetail) => { ...details.map((mailboxDetail) => {
const inEditMode = editingFolderForMailGroup === mailboxDetail.mailGroup._id 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 { private createMailboxFolderItems(mailboxDetail: MailboxDetail, inEditMode: boolean, onEditMailbox: () => void): Children {
return m(MailFoldersView, { return m(MailFoldersView, {
mailModel: mailLocator.mailModel,
mailboxDetail, mailboxDetail,
expandedFolders: this.expandedState, expandedFolders: this.expandedState,
mailFolderElementIdToSelectedMailId: this.mailViewModel.getMailFolderToSelectedMail(), mailFolderElementIdToSelectedMailId: this.mailViewModel.getMailFolderToSelectedMail(),
@ -628,7 +633,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
if (location.hash.length > 5) { if (location.hash.length > 5) {
let url = location.hash.substring(5) let url = location.hash.substring(5)
let decodedUrl = decodeURIComponent(url) let decodedUrl = decodeURIComponent(url)
Promise.all([locator.mailModel.getUserMailboxDetails(), import("../editor/MailEditor")]).then( Promise.all([locator.mailboxModel.getUserMailboxDetails(), import("../editor/MailEditor")]).then(
([mailboxDetails, { newMailtoUrlMailEditor }]) => { ([mailboxDetails, { newMailtoUrlMailEditor }]) => {
newMailtoUrlMailEditor(decodedUrl, false, mailboxDetails) newMailtoUrlMailEditor(decodedUrl, false, mailboxDetails)
.then((editor) => editor.show()) .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> { 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 // remove any selection to avoid that the next mail is loaded and selected for each deleted mail event
this.mailViewModel?.listModel?.selectNone() 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(() => const confirmed = await Dialog.confirm(() =>
lang.get("confirmDeleteFinallyCustomFolder_msg", { lang.get("confirmDeleteFinallyCustomFolder_msg", {
"{1}": getFolderName(folder), "{1}": getFolderName(folder),
}), }),
) )
if (!confirmed) return if (!confirmed) return
await locator.mailModel.finallyDeleteCustomMailFolder(folder) await mailLocator.mailModel.finallyDeleteCustomMailFolder(folder)
} else { } else {
const confirmed = await Dialog.confirm(() => const confirmed = await Dialog.confirm(() =>
lang.get("confirmDeleteCustomFolder_msg", { lang.get("confirmDeleteCustomFolder_msg", {
@ -729,7 +738,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
}), }),
) )
if (!confirmed) return 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 return
} }
// set all selected emails to the opposite of the first email's unread state // 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> { 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) { 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) await showEditFolderDialog(mailboxDetail, folder, parentFolder)
} }
} }

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import m, { Children, Component, Vnode } from "mithril" 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 { Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { IconButton } from "../../../common/gui/base/IconButton.js" import { IconButton } from "../../../common/gui/base/IconButton.js"
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.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 { ExpanderButton, ExpanderPanel } from "../../../common/gui/base/Expander.js"
import stream from "mithril/stream" import stream from "mithril/stream"
import { exportMails } from "../export/Exporter.js" 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 note that mailViewerViewModel has a mailModel, so you do not need to pass both if you pass a mailViewerViewModel
*/ */
export interface MailViewerToolbarAttrs { export interface MailViewerToolbarAttrs {
mailboxModel: MailboxModel
mailModel: MailModel mailModel: MailModel
mailViewerViewModel?: MailViewerViewModel mailViewerViewModel?: MailViewerViewModel
mails: Mail[] mails: Mail[]
@ -50,13 +52,13 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
} else if (attrs.mailViewerViewModel) { } else if (attrs.mailViewerViewModel) {
return [ return [
this.renderDeleteButton(mailModel, attrs.mails, attrs.selectNone ?? noOp), 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), attrs.mailViewerViewModel.isDraftMail() ? null : this.renderReadButton(attrs),
] ]
} else if (attrs.mails.length > 0) { } else if (attrs.mails.length > 0) {
return [ return [
this.renderDeleteButton(mailModel, attrs.mails, attrs.selectNone ?? noOp), 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.renderReadButton(attrs),
this.renderExportButton(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, { return m(IconButton, {
title: "move_action", title: "move_action",
icon: Icons.Folder, 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, OperationType,
} from "../../../common/api/common/TutanotaConstants" } from "../../../common/api/common/TutanotaConstants"
import { EntityClient } from "../../../common/api/common/EntityClient" 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 { ContactModel } from "../../../common/contactsFunctionality/ContactModel.js"
import { ConfigurationDatabase } from "../../../common/api/worker/facades/lazy/ConfigurationDatabase.js" import { ConfigurationDatabase } from "../../../common/api/worker/facades/lazy/ConfigurationDatabase.js"
import stream from "mithril/stream" import stream from "mithril/stream"
@ -43,7 +43,7 @@ import { LoginController } from "../../../common/api/main/LoginController"
import m from "mithril" import m from "mithril"
import { LockedError, NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError" import { LockedError, NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError"
import { haveSameId, isSameId } from "../../../common/api/common/utils/EntityUtils" 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 { SanitizedFragment } from "../../../common/misc/HtmlSanitizer"
import { CALENDAR_MIME_TYPE, FileController } from "../../../common/file/FileController" import { CALENDAR_MIME_TYPE, FileController } from "../../../common/file/FileController"
import { exportMails } from "../export/Exporter.js" 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 type { ContactImporter } from "../../contacts/ContactImporter.js"
import { InlineImages, revokeInlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js" import { InlineImages, revokeInlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
import { import {
assertSystemFolderOfType,
getDefaultSender, getDefaultSender,
getEnabledMailAddressesWithUser, getEnabledMailAddressesWithUser,
getFolderName, getFolderName,
getMailboxName, getMailboxName,
getPathToFolderString, getPathToFolderString,
isNoReplyTeamAddress,
isSystemNotification,
isTutanotaTeamMail,
loadMailDetails, loadMailDetails,
loadMailHeaders, loadMailHeaders,
} from "../../../common/mailFunctionality/SharedMailUtils.js" } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { isSystemNotification, isNoReplyTeamAddress } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../../common/api/common/CommonMailUtils.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 { CalendarModel } from "../../../calendar-app/calendar/model/CalendarModel.js"
import { mailLocator } from "../../mailLocator.js"
export const enum ContentBlockingStatus { export const enum ContentBlockingStatus {
Block = "0", Block = "0",
@ -137,6 +136,7 @@ export class MailViewerViewModel {
private _mail: Mail, private _mail: Mail,
showFolder: boolean, showFolder: boolean,
readonly entityClient: EntityClient, readonly entityClient: EntityClient,
public readonly mailboxModel: MailboxModel,
public readonly mailModel: MailModel, public readonly mailModel: MailModel,
readonly contactModel: ContactModel, readonly contactModel: ContactModel,
private readonly configFacade: ConfigurationDatabase, private readonly configFacade: ConfigurationDatabase,
@ -149,7 +149,6 @@ export class MailViewerViewModel {
private readonly mailFacade: MailFacade, private readonly mailFacade: MailFacade,
private readonly cryptoFacade: CryptoFacade, private readonly cryptoFacade: CryptoFacade,
private readonly contactImporter: lazyAsync<ContactImporter>, private readonly contactImporter: lazyAsync<ContactImporter>,
private readonly calendarModel: lazyAsync<CalendarModel>,
) { ) {
this.folderMailboxText = null this.folderMailboxText = null
if (showFolder) { if (showFolder) {
@ -206,10 +205,11 @@ export class MailViewerViewModel {
if (folder) { if (folder) {
this.mailModel.getMailboxDetailsForMail(this.mail).then((mailboxDetails) => { this.mailModel.getMailboxDetailsForMail(this.mail).then((mailboxDetails) => {
if (mailboxDetails == null) { if (mailboxDetails == null || mailboxDetails.mailbox.folders == null) {
return 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}` this.folderMailboxText = `${getMailboxName(this.logins, mailboxDetails)} / ${name}`
m.redraw() m.redraw()
}) })
@ -513,12 +513,19 @@ export class MailViewerViewModel {
await this.entityClient.update(this.mail) await this.entityClient.update(this.mail)
} }
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(this.mail) const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(this.mail)
if (mailboxDetail == null) { if (mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
return 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 // 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) { } catch (e) {
if (e instanceof NotFoundError) { if (e instanceof NotFoundError) {
console.log("mail already moved") console.log("mail already moved")
@ -1055,7 +1062,7 @@ export class MailViewerViewModel {
const { importCalendarFile, parseCalendarFile } = await import("../../../common/calendar/import/CalendarImporter.js") const { importCalendarFile, parseCalendarFile } = await import("../../../common/calendar/import/CalendarImporter.js")
const dataFile = await this.fileController.getAsDataFile(file) const dataFile = await this.fileController.getAsDataFile(file)
const data = parseCalendarFile(dataFile) 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) { } catch (e) {
console.log(e) console.log(e)
throw new UserError("errorDuringFileOpen_msg") 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 { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js" import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
import { IconButton } from "../../../common/gui/base/IconButton.js" import { IconButton } from "../../../common/gui/base/IconButton.js"
import { mailLocator } from "../../mailLocator.js"
const COUNTER_POS_OFFSET = px(-8) const COUNTER_POS_OFFSET = px(-8)
export type MinimizedEditorOverlayAttrs = { export type MinimizedEditorOverlayAttrs = {
@ -107,7 +108,7 @@ export class MinimizedEditorOverlay implements Component<MinimizedEditorOverlayA
const draft = model.draft const draft = model.draft
if (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, { return m(IconButton, {
title: "move_action", title: "move_action",
click: (e, dom) => click: (e, dom) =>
showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail], { showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail], {
width: this.dropdownWidth(), width: this.dropdownWidth(),
withBackground: true, withBackground: true,
}), }),

View file

@ -5,11 +5,13 @@ import { Icons } from "../../../common/gui/base/icons/Icons.js"
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js" import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
import { DROPDOWN_MARGIN } from "../../../common/gui/base/Dropdown.js" import { DROPDOWN_MARGIN } from "../../../common/gui/base/Dropdown.js"
import { MobileBottomActionBar } from "../../../common/gui/MobileBottomActionBar.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 { export interface MobileMailMultiselectionActionBarAttrs {
mails: readonly Mail[] mails: readonly Mail[]
mailModel: MailModel mailModel: MailModel
mailboxModel: MailboxModel
selectNone: () => unknown selectNone: () => unknown
} }
@ -17,7 +19,7 @@ export class MobileMailMultiselectionActionBar {
private dom: HTMLElement | null = null private dom: HTMLElement | null = null
view({ attrs }: Vnode<MobileMailMultiselectionActionBarAttrs>): Children { view({ attrs }: Vnode<MobileMailMultiselectionActionBarAttrs>): Children {
const { mails, selectNone, mailModel } = attrs const { mails, selectNone, mailModel, mailboxModel } = attrs
return m( return m(
MobileBottomActionBar, MobileBottomActionBar,
{ {
@ -35,7 +37,7 @@ export class MobileMailMultiselectionActionBar {
title: "move_action", title: "move_action",
click: (e, dom) => { click: (e, dom) => {
const referenceDom = this.dom ?? dom const referenceDom = this.dom ?? dom
showMoveMailsDropdown(mailModel, referenceDom.getBoundingClientRect(), mails, { showMoveMailsDropdown(mailboxModel, mailModel, referenceDom.getBoundingClientRect(), mails, {
onSelected: () => selectNone, onSelected: () => selectNone,
width: referenceDom.offsetWidth - DROPDOWN_MARGIN * 2, 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 { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js"
import { EventController } from "../common/api/main/EventController.js" import { EventController } from "../common/api/main/EventController.js"
import { SearchModel } from "./search/model/SearchModel.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 { MinimizedMailEditorViewModel } from "./mail/model/MinimizedMailEditorViewModel.js"
import { ContactModel } from "../common/contactsFunctionality/ContactModel.js" import { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
import { EntityClient } from "../common/api/common/EntityClient.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 { SearchRouter } from "../common/search/view/SearchRouter.js"
import { MailOpenedListener } from "./mail/view/MailViewModel.js" import { MailOpenedListener } from "./mail/view/MailViewModel.js"
import { getEnabledMailAddressesWithUser } from "../common/mailFunctionality/SharedMailUtils.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 { ShareableGroupType } from "../common/sharing/GroupUtils.js"
import { ReceivedGroupInvitationsModel } from "../common/sharing/model/ReceivedGroupInvitationsModel.js" import { ReceivedGroupInvitationsModel } from "../common/sharing/model/ReceivedGroupInvitationsModel.js"
import { CalendarViewModel } from "../calendar-app/calendar/view/CalendarViewModel.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 { AppStorePaymentPicker } from "../common/misc/AppStorePaymentPicker.js"
import { MAIL_PREFIX } from "../common/misc/RouteChange.js" import { MAIL_PREFIX } from "../common/misc/RouteChange.js"
import { getDisplayedSender } from "../common/api/common/CommonMailUtils.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 { AppType } from "../common/misc/ClientConstants.js"
import type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js" import type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js"
import type { ContactImporter } from "./contacts/ContactImporter.js" import type { ContactImporter } from "./contacts/ContactImporter.js"
import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js" import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js"
import { locator } from "../common/api/main/CommonLocator.js" import m from "mithril"
assertMainOrNode() assertMainOrNode()
class MailLocator { class MailLocator {
eventController!: EventController eventController!: EventController
search!: SearchModel search!: SearchModel
mailboxModel!: MailboxModel
mailModel!: MailModel mailModel!: MailModel
minimizedMailModel!: MinimizedMailEditorViewModel minimizedMailModel!: MinimizedMailEditorViewModel
contactModel!: ContactModel contactModel!: ContactModel
@ -224,6 +226,7 @@ class MailLocator {
const conversationViewModelFactory = await this.conversationViewModelFactory() const conversationViewModelFactory = await this.conversationViewModelFactory()
const router = new ScopedRouter(this.throttledRouter(), "/mail") const router = new ScopedRouter(this.throttledRouter(), "/mail")
return new MailViewModel( return new MailViewModel(
this.mailboxModel,
this.mailModel, this.mailModel,
this.entityClient, this.entityClient,
this.eventController, this.eventController,
@ -257,7 +260,7 @@ class MailLocator {
searchRouter, searchRouter,
this.search, this.search,
this.searchFacade, this.searchFacade,
this.mailModel, this.mailboxModel,
this.logins, this.logins,
this.indexerFacade, this.indexerFacade,
this.entityClient, this.entityClient,
@ -325,8 +328,8 @@ class MailLocator {
return new CalendarViewModel( return new CalendarViewModel(
this.logins, this.logins,
async (mode: CalendarOperation, event: CalendarEvent) => { async (mode: CalendarOperation, event: CalendarEvent) => {
const mailboxDetail = await this.mailModel.getUserMailboxDetails() const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot) const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null) return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null)
}, },
(...args) => this.calendarEventPreviewModel(...args), (...args) => this.calendarEventPreviewModel(...args),
@ -338,7 +341,7 @@ class MailLocator {
deviceConfig, deviceConfig,
await this.receivedGroupInvitationsModel(GroupType.Calendar), await this.receivedGroupInvitationsModel(GroupType.Calendar),
timeZone, timeZone,
this.mailModel, this.mailboxModel,
) )
}) })
@ -359,13 +362,16 @@ class MailLocator {
this.mailFacade, this.mailFacade,
this.entityClient, this.entityClient,
this.logins, this.logins,
this.mailModel, this.mailboxModel,
this.contactModel, this.contactModel,
this.eventController, this.eventController,
mailboxDetails, mailboxDetails,
recipientsModel, recipientsModel,
dateProvider, dateProvider,
mailboxProperties, mailboxProperties,
async (mail: Mail) => {
return await isMailInSpamOrTrash(mail)
},
) )
} }
@ -443,13 +449,14 @@ class MailLocator {
mail, mail,
showFolder, showFolder,
this.entityClient, this.entityClient,
this.mailboxModel,
this.mailModel, this.mailModel,
this.contactModel, this.contactModel,
this.configFacade, this.configFacade,
this.fileController, this.fileController,
this.logins, this.logins,
async (mailboxDetails) => { async (mailboxDetails) => {
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
return this.sendMailModel(mailboxDetails, mailboxProperties) return this.sendMailModel(mailboxDetails, mailboxProperties)
}, },
this.eventController, this.eventController,
@ -458,7 +465,6 @@ class MailLocator {
this.mailFacade, this.mailFacade,
this.cryptoFacade, this.cryptoFacade,
() => this.contactImporter(), () => this.contactImporter(),
() => this.calendarModel(),
) )
} }
@ -539,7 +545,7 @@ class MailLocator {
async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> { async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> {
const { OwnMailAddressNameChanger } = await import("../mail-app/settings/mailaddress/OwnMailAddressNameChanger.js") 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> { async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
@ -685,12 +691,14 @@ class MailLocator {
this.entropyFacade = entropyFacade this.entropyFacade = entropyFacade
this.workerFacade = workerFacade this.workerFacade = workerFacade
this.connectivityModel = new WebsocketConnectivityModel(eventBus) this.connectivityModel = new WebsocketConnectivityModel(eventBus)
this.mailboxModel = new MailboxModel(this.eventController, this.entityClient, this.logins)
this.mailModel = new MailModel( this.mailModel = new MailModel(
notifications, notifications,
this.mailboxModel,
this.eventController, this.eventController,
this.mailFacade,
this.entityClient, this.entityClient,
this.logins, this.logins,
this.mailFacade,
this.connectivityModel, this.connectivityModel,
this.inboxRuleHanlder(), this.inboxRuleHanlder(),
) )
@ -729,18 +737,63 @@ class MailLocator {
const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js") const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js")
const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.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.nativeInterfaces = createNativeInterfaces(
this.webMobileFacade, this.webMobileFacade,
new WebDesktopFacade(this.logins, async () => this.native), new WebDesktopFacade(this.logins, async () => this.native),
new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig), new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig),
new WebCommonNativeFacade( new WebCommonNativeFacade(
this.logins, this.logins,
this.mailModel, this.mailboxModel,
this.usageTestController, this.usageTestController,
async () => this.fileApp, async () => this.fileApp,
async () => this.pushService, async () => this.pushService,
this.handleFileImport.bind(this), 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, AppType.Integrated,
), ),
cryptoFacade, cryptoFacade,
@ -861,7 +914,7 @@ class MailLocator {
this.logins, this.logins,
this.progressTracker, this.progressTracker,
this.entityClient, this.entityClient,
this.mailModel, this.mailboxModel,
this.calendarFacade, this.calendarFacade,
this.fileController, this.fileController,
timeZone, timeZone,
@ -873,7 +926,7 @@ class MailLocator {
readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => { readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => {
const { CalendarInviteHandler } = await import("../calendar-app/calendar/view/CalendarInvites.js") const { CalendarInviteHandler } = await import("../calendar-app/calendar/view/CalendarInvites.js")
const { calendarNotificationSender } = await import("../calendar-app/calendar/view/CalendarNotificationSender.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), this.sendMailModel(...arg),
) )
}) })
@ -938,9 +991,9 @@ class MailLocator {
const { getEventType } = await import("../calendar-app/calendar/gui/CalendarGuiUtils.js") const { getEventType } = await import("../calendar-app/calendar/gui/CalendarGuiUtils.js")
const { CalendarEventPreviewViewModel } = await import("../calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.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 userController = this.logins.getUserController()
const customer = await userController.loadCustomer() const customer = await userController.loadCustomer()
@ -989,7 +1042,7 @@ class MailLocator {
mailLocator.nativeContactsSyncManager()?.syncContacts() mailLocator.nativeContactsSyncManager()?.syncContacts()
}, },
async () => { async () => {
const calendarModel = await locator.calendarModel() const calendarModel = await mailLocator.calendarModel()
calendarModel.handleSyncExternalCalendars() calendarModel.handleSyncExternalCalendars()
}, },
) )

View file

@ -13,7 +13,7 @@ import { Icon } from "../../common/gui/base/Icon"
import { client } from "../../common/misc/ClientDetector" import { client } from "../../common/misc/ClientDetector"
import m, { Children, Component, Vnode } from "mithril" import m, { Children, Component, Vnode } from "mithril"
import { theme } from "../../common/gui/theme" 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 { locator } from "../../common/api/main/CommonLocator"
import { IndexingErrorReason } from "../../common/api/worker/search/SearchTypes" import { IndexingErrorReason } from "../../common/api/worker/search/SearchTypes"
import { companyTeamLabel } from "../../common/misc/ClientConstants.js" 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 { formatEventDuration } from "../../calendar-app/calendar/gui/CalendarGuiUtils.js"
import { getSenderOrRecipientHeading } from "../../common/mailFunctionality/SharedMailUtils.js" import { getSenderOrRecipientHeading } from "../../common/mailFunctionality/SharedMailUtils.js"
import { isTutanotaTeamMail } from "../../common/mailFunctionality/SharedMailUtils.js"
import { getContactListName } from "../../common/contactsFunctionality/ContactUtils.js" import { getContactListName } from "../../common/contactsFunctionality/ContactUtils.js"
type SearchBarOverlayAttrs = { 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 { YEAR_IN_MILLIS } from "@tutao/tutanota-utils/dist/DateUtils.js"
import { getIndentedFolderNameForDropdown } from "../../../common/mailFunctionality/SharedMailUtils.js" import { getIndentedFolderNameForDropdown } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { BottomNav } from "../../gui/BottomNav.js" import { BottomNav } from "../../gui/BottomNav.js"
import { mailLocator } from "../../mailLocator.js"
assertMainOrNode() assertMainOrNode()
@ -402,7 +403,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
const conversationViewModel = this.searchViewModel.conversationViewModel const conversationViewModel = this.searchViewModel.conversationViewModel
if (this.searchViewModel.listModel?.state.inMultiselect || !conversationViewModel) { if (this.searchViewModel.listModel?.state.inMultiselect || !conversationViewModel) {
const actions = m(MailViewerActions, { const actions = m(MailViewerActions, {
mailModel: locator.mailModel, mailboxModel: locator.mailboxModel,
mailModel: mailLocator.mailModel,
mails: selectedMails, mails: selectedMails,
selectNone: () => this.searchViewModel.listModel.selectNone(), selectNone: () => this.searchViewModel.listModel.selectNone(),
}) })
@ -435,6 +437,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
}) })
} else { } else {
const actions = m(MailViewerActions, { const actions = m(MailViewerActions, {
mailboxModel: conversationViewModel.primaryViewModel().mailboxModel,
mailModel: conversationViewModel.primaryViewModel().mailModel, mailModel: conversationViewModel.primaryViewModel().mailModel,
mailViewerViewModel: conversationViewModel.primaryViewModel(), mailViewerViewModel: conversationViewModel.primaryViewModel(),
mails: [conversationViewModel.primaryMail], mails: [conversationViewModel.primaryMail],
@ -603,7 +606,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
return m(MobileMailMultiselectionActionBar, { return m(MobileMailMultiselectionActionBar, {
mails: this.searchViewModel.getSelectedMails(), mails: this.searchViewModel.getSelectedMails(),
selectNone: () => this.searchViewModel.listModel.selectNone(), selectNone: () => this.searchViewModel.listModel.selectNone(),
mailModel: locator.mailModel, mailModel: mailLocator.mailModel,
mailboxModel: locator.mailboxModel,
}) })
} else if (this.viewSlider.focusedColumn === this.resultListColumn) { } else if (this.viewSlider.focusedColumn === this.resultListColumn) {
return m( return m(
@ -645,8 +649,9 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
] ]
for (const mailbox of mailboxes) { for (const mailbox of mailboxes) {
const folderStructures = mailLocator.mailModel.folders()
const mailboxIndex = mailboxes.indexOf(mailbox) const mailboxIndex = mailboxes.indexOf(mailbox)
const mailFolders = mailbox.folders.getIndentedList() const mailFolders = folderStructures[assertNotNull(mailbox.mailbox.folders)._id].getIndentedList()
for (const folderInfo of mailFolders) { for (const folderInfo of mailFolders) {
if (folderInfo.folder.folderType !== MailSetKind.SPAM) { if (folderInfo.folder.folderType !== MailSetKind.SPAM) {
const mailboxLabel = mailboxIndex === 0 ? "" : ` (${getGroupInfoDisplayName(mailbox.mailGroupInfo)})` const mailboxLabel = mailboxIndex === 0 ? "" : ` (${getGroupInfoDisplayName(mailbox.mailGroupInfo)})`
@ -990,8 +995,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
await showProgressDialog("pleaseWait_msg", calendarInfos) await showProgressDialog("pleaseWait_msg", calendarInfos)
} }
const mailboxDetails = await locator.mailModel.getUserMailboxDetails() const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null) const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null)
if (model) { if (model) {
@ -1027,7 +1032,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
const selectedMails = this.searchViewModel.getSelectedMails() const selectedMails = this.searchViewModel.getSelectedMails()
if (selectedMails.length > 0) { if (selectedMails.length > 0) {
showMoveMailsDropdown(locator.mailModel, getMoveMailBounds(), selectedMails, { showMoveMailsDropdown(locator.mailboxModel, mailLocator.mailModel, getMoveMailBounds(), selectedMails, {
onSelected: () => { onSelected: () => {
if (selectedMails.length > 1) { if (selectedMails.length > 1) {
this.searchViewModel.listModel.selectNone() this.searchViewModel.listModel.selectNone()
@ -1041,7 +1046,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
let selectedMails = this.searchViewModel.getSelectedMails() let selectedMails = this.searchViewModel.getSelectedMails()
if (selectedMails.length > 0) { 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() this.searchViewModel.listModel.selectNone()
} }
locator.mailModel.deleteMails(selected) mailLocator.mailModel.deleteMails(selected)
} }
}) })
} else if (isSameTypeRef(this.searchViewModel.searchedType, ContactTypeRef)) { } else if (isSameTypeRef(this.searchViewModel.searchedType, ContactTypeRef)) {
@ -1147,6 +1152,6 @@ function getCurrentSearchMode(): SearchCategoryTypes {
} }
async function newMailEditor(): Promise<Dialog> { 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) return newMailEditor(mailboxDetails)
} }

View file

@ -47,7 +47,7 @@ import {
SearchCategoryTypes, SearchCategoryTypes,
} from "../model/SearchUtils.js" } from "../model/SearchUtils.js"
import Stream from "mithril/stream" 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 { SearchFacade } from "../../../common/api/worker/search/SearchFacade.js"
import { LoginController } from "../../../common/api/main/LoginController.js" import { LoginController } from "../../../common/api/main/LoginController.js"
import { Indexer } from "../../../common/api/worker/search/Indexer.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 { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.js"
import { getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js" import { getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getStartOfTheWeekOffsetForUser } from "../../../common/calendar/date/CalendarUtils.js" import { getStartOfTheWeekOffsetForUser } from "../../../common/calendar/date/CalendarUtils.js"
import { mailLocator } from "../../mailLocator.js"
const SEARCH_PAGE_SIZE = 100 const SEARCH_PAGE_SIZE = 100
@ -124,7 +125,7 @@ export class SearchViewModel {
readonly router: SearchRouter, readonly router: SearchRouter,
private readonly search: SearchModel, private readonly search: SearchModel,
private readonly searchFacade: SearchFacade, private readonly searchFacade: SearchFacade,
private readonly mailModel: MailModel, private readonly mailboxModel: MailboxModel,
private readonly logins: LoginController, private readonly logins: LoginController,
private readonly indexerFacade: Indexer, private readonly indexerFacade: Indexer,
private readonly entityClient: EntityClient, 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) this.eventController.addEntityListener(this.entityEventsListener)
}) })
@ -552,11 +553,17 @@ export class SearchViewModel {
private onMailboxesChanged(mailboxes: MailboxDetail[]) { private onMailboxesChanged(mailboxes: MailboxDetail[]) {
this.mailboxes = mailboxes this.mailboxes = mailboxes
const folderStructures = mailLocator.mailModel.folders()
// if selected folder no longer exist select another one // if selected folder no longer exist select another one
const selectedMailFolder = this.selectedMailFolder const selectedMailFolder = this.selectedMailFolder
if (selectedMailFolder[0] && mailboxes.every((mailbox) => mailbox.folders.getFolderById(selectedMailFolder[0]) == null)) { if (
this.selectedMailFolder = [getElementId(assertNotNull(mailboxes[0].folders.getSystemFolderByType(MailSetKind.INBOX)))] 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 { getInboxRuleTypeNameMapping } from "../mail/model/InboxRuleHandler"
import type { InboxRule } from "../../common/api/entities/tutanota/TypeRefs.js" import type { InboxRule } from "../../common/api/entities/tutanota/TypeRefs.js"
import { createInboxRule } 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 stream from "mithril/stream"
import { DropDownSelector } from "../../common/gui/base/DropDownSelector.js" import { DropDownSelector } from "../../common/gui/base/DropDownSelector.js"
import { TextField } from "../../common/gui/base/TextField.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 { locator } from "../../common/api/main/CommonLocator"
import { isOfflineError } from "../../common/api/common/utils/ErrorUtils.js" import { isOfflineError } from "../../common/api/common/utils/ErrorUtils.js"
import { import {
assertSystemFolderOfType,
getExistingRuleForType, getExistingRuleForType,
getFolderName, getFolderName,
getIndentedFolderNameForDropdown, getIndentedFolderNameForDropdown,
getPathToFolderString, getPathToFolderString,
} from "../../common/mailFunctionality/SharedMailUtils.js" } 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() assertMainOrNode()
@ -32,8 +34,9 @@ export type InboxRuleTemplate = Pick<InboxRule, "type" | "value"> & { _id?: Inbo
export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemplate) { export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemplate) {
if (locator.logins.getUserController().isFreeAccount()) { if (locator.logins.getUserController().isFreeAccount()) {
showNotAvailableForFreeDialog() showNotAvailableForFreeDialog()
} else if (mailBoxDetail) { } else if (mailBoxDetail && mailBoxDetail.mailbox.folders) {
let targetFolders = mailBoxDetail.folders.getIndentedList().map((folderInfo) => { const folders = mailLocator.mailModel.getMailboxFoldersForId(mailBoxDetail.mailbox.folders._id)
let targetFolders = folders.getIndentedList().map((folderInfo: IndentedFolder) => {
return { return {
name: getIndentedFolderNameForDropdown(folderInfo), name: getIndentedFolderNameForDropdown(folderInfo),
value: folderInfo.folder, value: folderInfo.folder,
@ -41,8 +44,8 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
}) })
const inboxRuleType = stream(ruleOrTemplate.type) const inboxRuleType = stream(ruleOrTemplate.type)
const inboxRuleValue = stream(ruleOrTemplate.value) const inboxRuleValue = stream(ruleOrTemplate.value)
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : mailBoxDetail.folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder)) const selectedFolder = ruleOrTemplate.targetFolder == null ? null : folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder))
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(mailBoxDetail.folders, MailSetKind.ARCHIVE)) const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(folders, MailSetKind.ARCHIVE))
let form = () => [ let form = () => [
m(DropDownSelector, { m(DropDownSelector, {
@ -66,7 +69,7 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
selectedValue: inboxRuleTarget(), selectedValue: inboxRuleTarget(),
selectedValueDisplay: getFolderName(inboxRuleTarget()), selectedValueDisplay: getFolderName(inboxRuleTarget()),
selectionChangedHandler: inboxRuleTarget, selectionChangedHandler: inboxRuleTarget,
helpLabel: () => getPathToFolderString(mailBoxDetail.folders, inboxRuleTarget(), true), helpLabel: () => getPathToFolderString(folders, inboxRuleTarget(), true),
}), }),
] ]

View file

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

View file

@ -1,12 +1,12 @@
import { AddressToName, MailAddressNameChanger } from "../../../common/settings/mailaddress/MailAddressTableModel.js" 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 { createMailAddressProperties, MailboxProperties } from "../../../common/api/entities/tutanota/TypeRefs.js"
import { EntityClient } from "../../../common/api/common/EntityClient.js" import { EntityClient } from "../../../common/api/common/EntityClient.js"
import { findAndRemove } from "@tutao/tutanota-utils" import { findAndRemove } from "@tutao/tutanota-utils"
/** Name changer for personal mailbox of the currently logged-in user. */ /** Name changer for personal mailbox of the currently logged-in user. */
export class OwnMailAddressNameChanger implements MailAddressNameChanger { 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> { async getSenderNames(): Promise<AddressToName> {
const mailboxProperties = await this.getMailboxProperties() const mailboxProperties = await this.getMailboxProperties()
@ -14,8 +14,8 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
} }
async setSenderName(address: string, name: string): Promise<AddressToName> { async setSenderName(address: string, name: string): Promise<AddressToName> {
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)
let aliasConfig = mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === address) let aliasConfig = mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === address)
if (aliasConfig == null) { if (aliasConfig == null) {
aliasConfig = createMailAddressProperties({ mailAddress: address, senderName: name }) aliasConfig = createMailAddressProperties({ mailAddress: address, senderName: name })
@ -28,8 +28,8 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
} }
async removeSenderName(address: string): Promise<AddressToName> { async removeSenderName(address: string): Promise<AddressToName> {
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)
findAndRemove(mailboxProperties.mailAddressProperties, (p) => p.mailAddress === address) findAndRemove(mailboxProperties.mailAddressProperties, (p) => p.mailAddress === address)
await this.entityClient.update(mailboxProperties) await this.entityClient.update(mailboxProperties)
return this.collectMap(mailboxProperties) return this.collectMap(mailboxProperties)
@ -44,7 +44,7 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
} }
private async getMailboxProperties(): Promise<MailboxProperties> { private async getMailboxProperties(): Promise<MailboxProperties> {
const mailboxDetails = await this.mailModel.getUserMailboxDetails() const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
return await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot) 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 { Icons } from "../../../../../src/common/gui/base/icons/Icons.js"
import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError.js" import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError.js"
import { getConfidentialIcon } from "../../../../../src/common/mailFunctionality/SharedMailUtils.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 { getDisplayedSender } from "../../../../../src/common/api/common/CommonMailUtils.js"
import { isTutanotaTeamAddress, isTutanotaTeamMail } from "../../../../../src/mail-app/mail/view/MailGuiUtils.js"
o.spec("MailUtilsTest", function () { o.spec("MailUtilsTest", function () {
function createSystemMail(overrides: Partial<Mail> = {}): Mail { function createSystemMail(overrides: Partial<Mail> = {}): Mail {

View file

@ -8,7 +8,7 @@ import { getFromMap, remove } from "@tutao/tutanota-utils"
class FakeWindow { class FakeWindow {
listeners: Map<string, ((e: unknown) => unknown)[]> = new Map() 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) this.getListeners(event).push(listener)
} }
@ -16,7 +16,7 @@ class FakeWindow {
return getFromMap(this.listeners, event, () => []) return getFromMap(this.listeners, event, () => [])
} }
removeEventListener: (typeof Window.prototype)["removeEventListener"] = (event, listener) => { removeEventListener: typeof Window.prototype["removeEventListener"] = (event, listener) => {
remove(this.getListeners(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 { getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (array) { if (array) {
array[0] = 32 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 { instance, matchers, when } from "testdouble"
import { CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js" import { CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js"
import { LoginController } from "../../../src/common/api/main/LoginController.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 { GroupInfoTypeRef, GroupTypeRef, User } from "../../../src/common/api/entities/sys/TypeRefs.js"
import { calendars, makeUserController } from "./CalendarTestUtils.js" import { calendars, makeUserController } from "./CalendarTestUtils.js"
import { UserController } from "../../../src/common/api/main/UserController.js" import { UserController } from "../../../src/common/api/main/UserController.js"
import { CalendarNotificationSender } from "../../../src/calendar-app/calendar/view/CalendarNotificationSender.js" import { CalendarNotificationSender } from "../../../src/calendar-app/calendar/view/CalendarNotificationSender.js"
import { mockAttribute } from "@tutao/tutanota-test-utils" import { mockAttribute } from "@tutao/tutanota-test-utils"
import { SendMailModel } from "../../../src/common/mailFunctionality/SendMailModel.js" 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 const { anything, argThat } = matchers
o.spec("CalendarInviteHandlerTest", function () { 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 calendarNotificationSender: CalendarNotificationSender
let mailboxDetails: MailboxDetail
o.beforeEach(function () { o.beforeEach(function () {
const customerId = "customerId" const customerId = "customerId"
@ -42,7 +47,7 @@ o.spec("CalendarInviteHandlerTest", function () {
const userSettingsGroupRoot = createTestEntity(UserSettingsGroupRootTypeRef) const userSettingsGroupRoot = createTestEntity(UserSettingsGroupRootTypeRef)
let userController: Partial<UserController> = makeUserController([], AccountType.FREE, undefined, false, false, user, userSettingsGroupRoot) let userController: Partial<UserController> = makeUserController([], AccountType.FREE, undefined, false, false, user, userSettingsGroupRoot)
const mailboxDetails: MailboxDetail = { mailboxDetails = {
mailbox: createTestEntity(MailBoxTypeRef), mailbox: createTestEntity(MailBoxTypeRef),
folders: new FolderSystem([]), folders: new FolderSystem([]),
mailGroupInfo: createTestEntity(GroupInfoTypeRef, { mailGroupInfo: createTestEntity(GroupInfoTypeRef, {
@ -53,9 +58,8 @@ o.spec("CalendarInviteHandlerTest", function () {
} }
const mailboxProperties: MailboxProperties = createTestEntity(MailboxPropertiesTypeRef, {}) const mailboxProperties: MailboxProperties = createTestEntity(MailboxPropertiesTypeRef, {})
mailModel = instance(MailModel) maiboxModel = instance(MailboxModel)
when(mailModel.getMailboxDetailsForMail(anything())).thenResolve(mailboxDetails) when(maiboxModel.getMailboxProperties(anything())).thenResolve(mailboxProperties)
when(mailModel.getMailboxProperties(anything())).thenResolve(mailboxProperties)
calendarModel = instance(CalendarModel) calendarModel = instance(CalendarModel)
when(calendarModel.getEventsByUid(anything())).thenResolve({ when(calendarModel.getEventsByUid(anything())).thenResolve({
@ -73,7 +77,7 @@ o.spec("CalendarInviteHandlerTest", function () {
sendMailModel = instance(SendMailModel) sendMailModel = instance(SendMailModel)
calendarIniviteHandler = new CalendarInviteHandler(mailModel, calendarModel, logins, calendarNotificationSender, async () => { calendarIniviteHandler = new CalendarInviteHandler(maiboxModel, calendarModel, logins, calendarNotificationSender, async () => {
return sendMailModel return sendMailModel
}) })
}) })
@ -103,7 +107,9 @@ o.spec("CalendarInviteHandlerTest", function () {
let mail = createTestEntity(MailTypeRef) let mail = createTestEntity(MailTypeRef)
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null }) mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
when(calendarModel.getCalendarInfos()).thenResolve(calendars) 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) o(calendarModel.processCalendarEventMessage.callCount).equals(1)
}) })
@ -131,7 +137,9 @@ o.spec("CalendarInviteHandlerTest", function () {
let mail = createTestEntity(MailTypeRef) let mail = createTestEntity(MailTypeRef)
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null }) mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
when(calendarModel.getCalendarInfos()).thenResolve(calendars) 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) o(calendarModel.processCalendarEventMessage.callCount).equals(0)
}) })
@ -160,7 +168,9 @@ o.spec("CalendarInviteHandlerTest", function () {
let mail = createTestEntity(MailTypeRef) let mail = createTestEntity(MailTypeRef)
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null }) mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
when(calendarModel.getCalendarInfos()).thenResolve(new Map()) 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) 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 { NoopProgressMonitor } from "../../../src/common/api/common/utils/ProgressMonitor.js"
import { makeAlarmScheduler } from "./CalendarTestUtils.js" import { makeAlarmScheduler } from "./CalendarTestUtils.js"
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.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 { incrementByRepeatPeriod } from "../../../src/common/calendar/date/CalendarUtils.js"
import { ExternalCalendarFacade } from "../../../src/common/native/common/generatedipc/ExternalCalendarFacade.js" import { ExternalCalendarFacade } from "../../../src/common/native/common/generatedipc/ExternalCalendarFacade.js"
import { DeviceConfig } from "../../../src/common/misc/DeviceConfig.js" import { DeviceConfig } from "../../../src/common/misc/DeviceConfig.js"
@ -728,7 +728,7 @@ function makeLoginController(): LoginController {
return loginController return loginController
} }
function makeMailModel(): MailModel { function makeMailModel(): MailboxModel {
return downcast({}) return downcast({})
} }

View file

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

View file

@ -24,7 +24,7 @@ import {
} from "../../../src/calendar-app/calendar/view/CalendarViewModel.js" } from "../../../src/calendar-app/calendar/view/CalendarViewModel.js"
import { CalendarInfo, CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js" import { CalendarInfo, CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js"
import { CalendarEventsRepository, DaysToEvents } from "../../../src/common/calendar/date/CalendarEventsRepository.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 { addDaysForEventInstance, getMonthRange } from "../../../src/common/calendar/date/CalendarUtils.js"
import { CalendarEventModel, CalendarOperation, EventSaveResult } from "../../../src/calendar-app/calendar/gui/eventeditor-model/CalendarEventModel.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, getUserController: () => userController,
isInternalUserLoggedIn: () => true, isInternalUserLoggedIn: () => true,
}) })
const mailModel: MailModel = object() const mailboxModel: MailboxModel = object()
const previewModelFactory: CalendarEventPreviewModelFactory = async () => object() const previewModelFactory: CalendarEventPreviewModelFactory = async () => object()
const viewModel = new CalendarViewModel( const viewModel = new CalendarViewModel(
loginController, loginController,
@ -86,7 +86,7 @@ o.spec("CalendarViewModel", function () {
deviceConfig, deviceConfig,
calendarInvitations, calendarInvitations,
zone, zone,
mailModel, mailboxModel,
) )
viewModel.allowDrag = () => true viewModel.allowDrag = () => true
return { viewModel, calendarModel, eventsRepository } 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 { areExcludedDatesEqual, areRepeatRulesEqual } from "../../../../src/common/calendar/date/CalendarUtils.js"
import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js" import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js"
import { FolderSystem } from "../../../../src/common/api/common/mail/FolderSystem.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 () { o.spec("CalendarEventModelTest", function () {
let userController: UserController 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 { MailFolderTypeRef, MailTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
import { EntityClient } from "../../../src/common/api/common/EntityClient.js" import { EntityClient } from "../../../src/common/api/common/EntityClient.js"
import { EntityRestClientMock } from "../api/worker/rest/EntityRestClientMock.js" import { EntityRestClientMock } from "../api/worker/rest/EntityRestClientMock.js"
import nodemocker from "../nodemocker.js"
import { downcast } from "@tutao/tutanota-utils" 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 { LoginController } from "../../../src/common/api/main/LoginController.js"
import { matchers, object, when } from "testdouble" 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 { UserController } from "../../../src/common/api/main/UserController.js"
import { createTestEntity } from "../TestUtils.js" import { createTestEntity } from "../TestUtils.js"
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.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 { InboxRuleHandler } from "../../../src/mail-app/mail/model/InboxRuleHandler.js"
import { getElementId, getListId } from "../../../src/common/api/common/utils/EntityUtils.js" import { getElementId, getListId } from "../../../src/common/api/common/utils/EntityUtils.js"
o.spec("MailModelTest", function () { o.spec("MailModelTest", function () {
let notifications: Partial<Notifications> let notifications: Partial<Notifications>
let showSpy: Spy let showSpy: Spy
let model: MailModel let model: MailboxModel
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"], isMailSet: false }) const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"], isMailSet: false })
inboxFolder.mails = "instanceListId" inboxFolder.mails = "instanceListId"
inboxFolder.folderType = MailSetKind.INBOX inboxFolder.folderType = MailSetKind.INBOX
@ -36,22 +32,15 @@ o.spec("MailModelTest", function () {
const restClient: EntityRestClientMock = new EntityRestClientMock() const restClient: EntityRestClientMock = new EntityRestClientMock()
o.beforeEach(function () { o.beforeEach(function () {
mailboxDetails = [
{
folders: new FolderSystem([inboxFolder, anotherFolder]),
},
]
notifications = {} notifications = {}
showSpy = notifications.showNotification = spy() showSpy = notifications.showNotification = spy()
const connectivityModel = object<WebsocketConnectivityModel>()
const mailFacade = nodemocker.mock<MailFacade>("mailFacade", {}).set()
logins = object() logins = object()
let userController = object<UserController>() let userController = object<UserController>()
when(userController.isUpdateForLoggedInUserInstance(matchers.anything(), matchers.anything())).thenReturn(false) when(userController.isUpdateForLoggedInUserInstance(matchers.anything(), matchers.anything())).thenReturn(false)
when(logins.getUserController()).thenReturn(userController) when(logins.getUserController()).thenReturn(userController)
inboxRuleHandler = object() 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 // not pretty, but works
model.mailboxDetails(mailboxDetails as MailboxDetail[]) model.mailboxDetails(mailboxDetails as MailboxDetail[])
}) })

View file

@ -11,6 +11,7 @@ import {
ConversationEntryTypeRef, ConversationEntryTypeRef,
createContact, createContact,
CustomerAccountCreateDataTypeRef, CustomerAccountCreateDataTypeRef,
Mail,
MailAddressTypeRef, MailAddressTypeRef,
MailboxGroupRootTypeRef, MailboxGroupRootTypeRef,
MailboxPropertiesTypeRef, 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 { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
import { createTestEntity } from "../TestUtils.js" import { createTestEntity } from "../TestUtils.js"
import { ContactModel } from "../../../src/common/contactsFunctionality/ContactModel.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 { SendMailModel, TOO_MANY_VISIBLE_RECIPIENTS } from "../../../src/common/mailFunctionality/SendMailModel.js"
import { RecipientField } from "../../../src/common/mailFunctionality/SharedMailUtils.js" import { RecipientField } from "../../../src/common/mailFunctionality/SharedMailUtils.js"
import { getContactDisplayName } from "../../../src/common/contactsFunctionality/ContactUtils.js" import { getContactDisplayName } from "../../../src/common/contactsFunctionality/ContactUtils.js"
@ -95,7 +96,7 @@ o.spec("SendMailModel", function () {
lang.init(en) lang.init(en)
}) })
let mailModel: MailModel, entity: EntityClient, mailFacade: MailFacade, recipientsModel: RecipientsModel let mailboxModel: MailboxModel, entity: EntityClient, mailFacade: MailFacade, recipientsModel: RecipientsModel
let model: SendMailModel let model: SendMailModel
@ -109,7 +110,7 @@ o.spec("SendMailModel", function () {
).thenDo(() => ({ contacts: testIdGenerator.newId() })) ).thenDo(() => ({ contacts: testIdGenerator.newId() }))
when(entity.load(anything(), anything(), anything())).thenDo((typeRef, id, params) => ({ _type: typeRef, _id: id })) when(entity.load(anything(), anything(), anything())).thenDo((typeRef, id, params) => ({ _type: typeRef, _id: id }))
mailModel = instance(MailModel) mailboxModel = instance(MailboxModel)
const contactModel = object<ContactModel>() const contactModel = object<ContactModel>()
when(contactModel.getContactListId()).thenResolve("contactListId") when(contactModel.getContactListId()).thenResolve("contactListId")
@ -153,7 +154,6 @@ o.spec("SendMailModel", function () {
const mailboxDetails: MailboxDetail = { const mailboxDetails: MailboxDetail = {
mailbox: createTestEntity(MailBoxTypeRef), mailbox: createTestEntity(MailBoxTypeRef),
folders: new FolderSystem([]),
mailGroupInfo: createTestEntity(GroupInfoTypeRef, { mailGroupInfo: createTestEntity(GroupInfoTypeRef, {
mailAddress: "mailgroup@addre.ss", mailAddress: "mailgroup@addre.ss",
}), }),
@ -180,13 +180,16 @@ o.spec("SendMailModel", function () {
mailFacade, mailFacade,
entity, entity,
loginController, loginController,
mailModel, mailboxModel,
contactModel, contactModel,
eventController, eventController,
mailboxDetails, mailboxDetails,
recipientsModel, recipientsModel,
new NoZoneDateProvider(), new NoZoneDateProvider(),
mailboxProperties, mailboxProperties,
async (mail: Mail) => {
return false
},
) )
replace(model, "getDefaultSender", () => DEFAULT_SENDER_FOR_TESTING) 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 { MailSetKind, MailState, OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
import { isSameId } from "../../../../src/common/api/common/utils/EntityUtils.js" import { isSameId } from "../../../../src/common/api/common/utils/EntityUtils.js"
import { createTestEntity } from "../../TestUtils.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 () { o.spec("ConversationViewModel", function () {
let conversation: ConversationEntry[] let conversation: ConversationEntry[]
@ -29,6 +30,7 @@ o.spec("ConversationViewModel", function () {
let viewModel: ConversationViewModel let viewModel: ConversationViewModel
let mailModel: MailModel let mailModel: MailModel
let mailboxModel: MailboxModel
let mailboxDetail: MailboxDetail let mailboxDetail: MailboxDetail
let entityRestClientMock: EntityRestClientMock let entityRestClientMock: EntityRestClientMock
let prefProvider: ConversationPrefProvider 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 { GroupInfoTypeRef } from "../../../../src/common/api/entities/sys/TypeRefs.js"
import { CryptoFacade } from "../../../../src/common/api/worker/crypto/CryptoFacade.js" import { CryptoFacade } from "../../../../src/common/api/worker/crypto/CryptoFacade.js"
import { ContactImporter } from "../../../../src/mail-app/contacts/ContactImporter.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 { ContactModel } from "../../../../src/common/contactsFunctionality/ContactModel.js"
import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.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 () { o.spec("MailViewerViewModel", function () {
let mail: Mail let mail: Mail
@ -34,6 +34,7 @@ o.spec("MailViewerViewModel", function () {
let entityClient: EntityClient let entityClient: EntityClient
let mailModel: MailModel let mailModel: MailModel
let mailboxModel: MailboxModel
let contactModel: ContactModel let contactModel: ContactModel
let configFacade: ConfigurationDatabase let configFacade: ConfigurationDatabase
let fileController: FileController let fileController: FileController
@ -46,11 +47,11 @@ o.spec("MailViewerViewModel", function () {
let sendMailModelFactory: (mailboxDetails: MailboxDetail) => Promise<SendMailModel> = () => Promise.resolve(sendMailModel) let sendMailModelFactory: (mailboxDetails: MailboxDetail) => Promise<SendMailModel> = () => Promise.resolve(sendMailModel)
let cryptoFacade: CryptoFacade let cryptoFacade: CryptoFacade
let contactImporter: ContactImporter let contactImporter: ContactImporter
let calendarModel: CalendarModel
function makeViewModelWithHeaders(headers: string) { function makeViewModelWithHeaders(headers: string) {
entityClient = object() entityClient = object()
mailModel = object() mailModel = object()
mailboxModel = object()
contactModel = object() contactModel = object()
configFacade = object() configFacade = object()
fileController = object() fileController = object()
@ -62,13 +63,13 @@ o.spec("MailViewerViewModel", function () {
mailFacade = object() mailFacade = object()
cryptoFacade = object() cryptoFacade = object()
contactImporter = object() contactImporter = object()
calendarModel = object()
mail = prepareMailWithHeaders(mailFacade, headers) mail = prepareMailWithHeaders(mailFacade, headers)
return new MailViewerViewModel( return new MailViewerViewModel(
mail, mail,
showFolder, showFolder,
entityClient, entityClient,
mailboxModel,
mailModel, mailModel,
contactModel, contactModel,
configFacade, configFacade,
@ -81,7 +82,6 @@ o.spec("MailViewerViewModel", function () {
mailFacade, mailFacade,
cryptoFacade, cryptoFacade,
async () => contactImporter, async () => contactImporter,
async () => calendarModel,
) )
} }