mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
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:
parent
f914cb2613
commit
88355bd5d1
65 changed files with 989 additions and 727 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}`)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
237
src/common/mailFunctionality/MailboxModel.ts
Normal file
237
src/common/mailFunctionality/MailboxModel.ts
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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({
|
||||||
|
|
|
@ -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?
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)) : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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[])
|
||||||
})
|
})
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue