mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 07:53:47 +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",
|
||||
})
|
||||
|
||||
type CodeValues = (typeof ansiSequences)[keyof typeof ansiSequences]
|
||||
type CodeValues = typeof ansiSequences[keyof typeof ansiSequences]
|
||||
|
||||
export function fancy(text: string, code: CodeValues) {
|
||||
if (typeof process !== "undefined" && process.stdout.isTTY) {
|
||||
|
|
|
@ -9,8 +9,8 @@ import { createFile } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
|||
import { convertToDataFile, DataFile } from "../../../common/api/common/DataFile"
|
||||
import type { DateWrapper, RepeatRule, UserAlarmInfo } from "../../../common/api/entities/sys/TypeRefs.js"
|
||||
import { DateTime } from "luxon"
|
||||
import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController"
|
||||
import { getLetId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { CALENDAR_MIME_TYPE } from "../../../common/file/FileController.js"
|
||||
|
||||
/** create an ical data file that can be attached to an invitation/update/cancellation/response mail */
|
||||
export function makeInvitationCalendarFile(event: CalendarEvent, method: CalendarMethod, now: Date, zone: string): DataFile {
|
||||
|
|
|
@ -65,7 +65,7 @@ import {
|
|||
MailboxProperties,
|
||||
} from "../../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { User } from "../../../../common/api/entities/sys/TypeRefs.js"
|
||||
import { MailboxDetail } from "../../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../../../common/mailFunctionality/MailboxModel.js"
|
||||
import {
|
||||
AlarmInterval,
|
||||
areRepeatRulesEqual,
|
||||
|
|
|
@ -48,7 +48,7 @@ import { ProgressTracker } from "../../../common/api/main/ProgressTracker"
|
|||
import type { IProgressMonitor } from "../../../common/api/common/utils/ProgressMonitor"
|
||||
import { NoopProgressMonitor } from "../../../common/api/common/utils/ProgressMonitor"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient"
|
||||
import type { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { elementIdPart, getElementId, isSameId, listIdPart, removeTechnicalFields } from "../../../common/api/common/utils/EntityUtils"
|
||||
import type { AlarmScheduler } from "../../../common/calendar/date/AlarmScheduler.js"
|
||||
import { Notifications, NotificationType } from "../../../common/gui/Notifications"
|
||||
|
@ -134,7 +134,7 @@ export class CalendarModel {
|
|||
private readonly logins: LoginController,
|
||||
private readonly progressTracker: ProgressTracker,
|
||||
private readonly entityClient: EntityClient,
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly calendarFacade: CalendarFacade,
|
||||
private readonly fileController: FileController,
|
||||
private readonly zone: string,
|
||||
|
@ -454,7 +454,7 @@ export class CalendarModel {
|
|||
}
|
||||
|
||||
private async loadAndProcessCalendarUpdates(): Promise<void> {
|
||||
const { mailboxGroupRoot } = await this.mailModel.getUserMailboxDetails()
|
||||
const { mailboxGroupRoot } = await this.mailboxModel.getUserMailboxDetails()
|
||||
const { calendarEventUpdates } = mailboxGroupRoot
|
||||
if (calendarEventUpdates == null) return
|
||||
|
||||
|
|
|
@ -470,8 +470,8 @@ export class CalendarSearchView extends BaseTopLevelView implements TopLevelView
|
|||
await showProgressDialog("pleaseWait_msg", calendarInfos)
|
||||
}
|
||||
|
||||
const mailboxDetails = await calendarLocator.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await calendarLocator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxDetails = await calendarLocator.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await calendarLocator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const model = await calendarLocator.calendarEventModel(
|
||||
CalendarOperation.Create,
|
||||
getEventWithDefaultTimes(dateToUse),
|
||||
|
|
|
@ -17,7 +17,7 @@ import { isCustomizationEnabledForCustomer } from "../../../common/api/common/ut
|
|||
import { getEventType } from "../gui/CalendarGuiUtils.js"
|
||||
import { CalendarModel } from "../model/CalendarModel.js"
|
||||
import { LoginController } from "../../../common/api/main/LoginController.js"
|
||||
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { SendMailModel } from "../../../common/mailFunctionality/SendMailModel.js"
|
||||
import { RecipientField } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
|
@ -70,10 +70,10 @@ export async function showEventDetails(event: CalendarEvent, eventBubbleRect: Cl
|
|||
} else {
|
||||
const [calendarInfos, mailboxDetails, customer] = await Promise.all([
|
||||
(await locator.calendarModel()).getCalendarInfos(),
|
||||
locator.mailModel.getUserMailboxDetails(),
|
||||
locator.mailboxModel.getUserMailboxDetails(),
|
||||
locator.logins.getUserController().loadCustomer(),
|
||||
])
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const ownMailAddresses = mailboxProperties.mailAddressProperties.map(({ mailAddress }) => mailAddress)
|
||||
ownAttendee = findAttendeeInAddresses(latestEvent.attendees, ownMailAddresses)
|
||||
eventType = getEventType(latestEvent, calendarInfos, ownMailAddresses, locator.logins.getUserController())
|
||||
|
@ -138,7 +138,7 @@ export const enum ReplyResult {
|
|||
|
||||
export class CalendarInviteHandler {
|
||||
constructor(
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly calendarModel: CalendarModel,
|
||||
private readonly logins: LoginController,
|
||||
private readonly calendarNotificationSender: CalendarNotificationSender,
|
||||
|
@ -157,13 +157,17 @@ export class CalendarInviteHandler {
|
|||
attendee: CalendarEventAttendee,
|
||||
decision: CalendarAttendeeStatus,
|
||||
previousMail: Mail,
|
||||
mailboxDetails: MailboxDetail,
|
||||
): Promise<ReplyResult> {
|
||||
const eventClone = clone(event)
|
||||
const foundAttendee = assertNotNull(findAttendeeInAddresses(eventClone.attendees, [attendee.address.address]), "attendee was not found in event clone")
|
||||
foundAttendee.status = decision
|
||||
|
||||
const notificationModel = new CalendarNotificationModel(this.calendarNotificationSender, this.logins)
|
||||
const responseModel = await this.getResponseModelForMail(previousMail, attendee.address.address)
|
||||
//NOTE: mailDetails are getting passed through because the calendar does not have access to the mail folder structure
|
||||
// which is needed to find mailboxdetails by mail. This may be fixed by static mail ids which are being worked on currently.
|
||||
// This function is only called by EventBanner from the mail app so this should be okay.
|
||||
const responseModel = await this.getResponseModelForMail(previousMail, mailboxDetails, attendee.address.address)
|
||||
|
||||
try {
|
||||
await notificationModel.send(eventClone, [], { responseModel, inviteModel: null, cancelModel: null, updateModel: null })
|
||||
|
@ -197,10 +201,10 @@ export class CalendarInviteHandler {
|
|||
return ReplyResult.ReplySent
|
||||
}
|
||||
|
||||
async getResponseModelForMail(previousMail: Mail, responder: string): Promise<SendMailModel | null> {
|
||||
const mailboxDetails = await this.mailModel.getMailboxDetailsForMail(previousMail)
|
||||
if (mailboxDetails == null) return null
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
async getResponseModelForMail(previousMail: Mail, mailboxDetails: MailboxDetail, responder: string): Promise<SendMailModel | null> {
|
||||
//NOTE: mailDetails are getting passed through because the calendar does not have access to the mail folder structure
|
||||
// which is needed to find mailboxdetails by mail. This may be fixed by static mail ids which are being worked on currently
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const model = await this.sendMailModelFactory(mailboxDetails, mailboxProperties)
|
||||
await model.initAsResponse(
|
||||
{
|
||||
|
|
|
@ -598,8 +598,8 @@ export class CalendarView extends BaseTopLevelView implements TopLevelView<Calen
|
|||
await showProgressDialog("pleaseWait_msg", calendarInfos)
|
||||
}
|
||||
|
||||
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null)
|
||||
if (model) {
|
||||
await showNewCalendarEventEditDialog(model)
|
||||
|
|
|
@ -24,7 +24,7 @@ import { Time } from "../../../common/calendar/date/Time.js"
|
|||
import { CalendarEventsRepository, DaysToEvents } from "../../../common/calendar/date/CalendarEventsRepository.js"
|
||||
import { CalendarEventPreviewViewModel } from "../gui/eventpopup/CalendarEventPreviewViewModel.js"
|
||||
import { EntityUpdateData, isUpdateFor, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { getEnabledMailAddressesWithUser } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
export type EventsOnDays = {
|
||||
|
@ -91,7 +91,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
|
|||
private readonly deviceConfig: DeviceConfig,
|
||||
private readonly calendarInvitationsModel: ReceivedGroupInvitationsModel<GroupType.Calendar>,
|
||||
private readonly timeZone: string,
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
) {
|
||||
this._transientEvents = []
|
||||
|
||||
|
@ -197,7 +197,7 @@ export class CalendarViewModel implements EventDragHandlerCallbacks {
|
|||
private canFullyEditEvent(event: CalendarEvent): boolean {
|
||||
const userController = this.logins.getUserController()
|
||||
const userMailGroup = userController.getUserMailGroupMembership().group
|
||||
const mailboxDetailsArray = this.mailModel.mailboxDetails()
|
||||
const mailboxDetailsArray = this.mailboxModel.mailboxDetails()
|
||||
const mailboxDetails = assertNotNull(mailboxDetailsArray.find((md) => md.mailGroup._id === userMailGroup))
|
||||
const ownMailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userController.userGroupInfo)
|
||||
const eventType = getEventType(event, this.calendarInfos, ownMailAddresses, userController)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js"
|
||||
import { EventController } from "../common/api/main/EventController.js"
|
||||
import { MailboxDetail, MailModel } from "../common/mailFunctionality/MailModel.js"
|
||||
import { type MailboxDetail, MailboxModel } from "../common/mailFunctionality/MailboxModel.js"
|
||||
import { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
|
||||
import { EntityClient } from "../common/api/common/EntityClient.js"
|
||||
import { ProgressTracker } from "../common/api/main/ProgressTracker.js"
|
||||
|
@ -110,13 +110,14 @@ import { AppType } from "../common/misc/ClientConstants.js"
|
|||
import type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js"
|
||||
import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js"
|
||||
import { locator } from "../common/api/main/CommonLocator.js"
|
||||
import m from "mithril"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
class CalendarLocator {
|
||||
eventController!: EventController
|
||||
search!: CalendarSearchModel
|
||||
mailModel!: MailModel
|
||||
mailboxModel!: MailboxModel
|
||||
contactModel!: ContactModel
|
||||
entityClient!: EntityClient
|
||||
progressTracker!: ProgressTracker
|
||||
|
@ -269,8 +270,8 @@ class CalendarLocator {
|
|||
return new CalendarViewModel(
|
||||
this.logins,
|
||||
async (mode: CalendarOperation, event: CalendarEvent) => {
|
||||
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
|
||||
const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
|
||||
return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null)
|
||||
},
|
||||
(...args) => this.calendarEventPreviewModel(...args),
|
||||
|
@ -282,7 +283,7 @@ class CalendarLocator {
|
|||
deviceConfig,
|
||||
await this.receivedGroupInvitationsModel(GroupType.Calendar),
|
||||
timeZone,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -303,13 +304,16 @@ class CalendarLocator {
|
|||
this.mailFacade,
|
||||
this.entityClient,
|
||||
this.logins,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.contactModel,
|
||||
this.eventController,
|
||||
mailboxDetails,
|
||||
recipientsModel,
|
||||
dateProvider,
|
||||
mailboxProperties,
|
||||
async (mail: Mail) => {
|
||||
return false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -425,7 +429,7 @@ class CalendarLocator {
|
|||
|
||||
async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> {
|
||||
const { OwnMailAddressNameChanger } = await import("../mail-app/settings/mailaddress/OwnMailAddressNameChanger.js")
|
||||
return new OwnMailAddressNameChanger(this.mailModel, this.entityClient)
|
||||
return new OwnMailAddressNameChanger(this.mailboxModel, this.entityClient)
|
||||
}
|
||||
|
||||
async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
|
||||
|
@ -571,7 +575,7 @@ class CalendarLocator {
|
|||
this.entropyFacade = entropyFacade
|
||||
this.workerFacade = workerFacade
|
||||
this.connectivityModel = new WebsocketConnectivityModel(eventBus)
|
||||
this.mailModel = new MailModel(notifications, this.eventController, this.mailFacade, this.entityClient, this.logins, null, null)
|
||||
this.mailboxModel = new MailboxModel(this.eventController, this.entityClient, this.logins)
|
||||
this.operationProgressTracker = new OperationProgressTracker()
|
||||
this.infoMessageHandler = new InfoMessageHandler((state: SearchIndexStateInfo) => {
|
||||
// calendar does not have index, so nothing needs to be handled here
|
||||
|
@ -607,18 +611,26 @@ class CalendarLocator {
|
|||
const { WebInterWindowEventFacade } = await import("../common/native/main/WebInterWindowEventFacade.js")
|
||||
const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js")
|
||||
const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.js")
|
||||
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailModel, CALENDAR_PREFIX)
|
||||
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailboxModel, CALENDAR_PREFIX)
|
||||
this.nativeInterfaces = createNativeInterfaces(
|
||||
this.webMobileFacade,
|
||||
new WebDesktopFacade(this.logins, async () => this.native),
|
||||
new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig),
|
||||
new WebCommonNativeFacade(
|
||||
this.logins,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.usageTestController,
|
||||
async () => this.fileApp,
|
||||
async () => this.pushService,
|
||||
this.handleFileImport.bind(this),
|
||||
async (userId: string, address: string, requestedPath: string | null) => noOp(),
|
||||
async (userId: string) => {
|
||||
if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) {
|
||||
m.route.set("/calendar/agenda")
|
||||
} else {
|
||||
m.route.set(`/login?noAutoLogin=false&userId=${userId}&requestedPath=${encodeURIComponent("/calendar/agenda")}`)
|
||||
}
|
||||
},
|
||||
AppType.Calendar,
|
||||
),
|
||||
cryptoFacade,
|
||||
|
@ -763,7 +775,7 @@ class CalendarLocator {
|
|||
this.logins,
|
||||
this.progressTracker,
|
||||
this.entityClient,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.calendarFacade,
|
||||
this.fileController,
|
||||
timeZone,
|
||||
|
@ -775,7 +787,7 @@ class CalendarLocator {
|
|||
readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => {
|
||||
const { CalendarInviteHandler } = await import("./calendar/view/CalendarInvites.js")
|
||||
const { calendarNotificationSender } = await import("./calendar/view/CalendarNotificationSender.js")
|
||||
return new CalendarInviteHandler(this.mailModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
|
||||
return new CalendarInviteHandler(this.mailboxModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
|
||||
this.sendMailModel(...arg),
|
||||
)
|
||||
})
|
||||
|
@ -797,9 +809,9 @@ class CalendarLocator {
|
|||
const { getEventType } = await import("./calendar/gui/CalendarGuiUtils.js")
|
||||
const { CalendarEventPreviewViewModel } = await import("./calendar/gui/eventpopup/CalendarEventPreviewViewModel.js")
|
||||
|
||||
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
|
||||
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
|
||||
const userController = this.logins.getUserController()
|
||||
const customer = await userController.loadCustomer()
|
||||
|
|
|
@ -1201,3 +1201,4 @@ export enum GroupKeyRotationType {
|
|||
export const GroupKeyRotationTypeNameByCode = reverse(GroupKeyRotationType)
|
||||
|
||||
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 { WorkerRandomizer } from "../worker/WorkerImpl.js"
|
||||
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js"
|
||||
import { MailboxDetail, MailModel } from "../../mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail, MailboxModel } from "../../mailFunctionality/MailboxModel.js"
|
||||
import { EventController } from "./EventController.js"
|
||||
import type { ContactModel } from "../../contactsFunctionality/ContactModel.js"
|
||||
import { ProgressTracker } from "./ProgressTracker.js"
|
||||
|
@ -115,7 +115,7 @@ export interface CommonLocator {
|
|||
random: WorkerRandomizer
|
||||
connectivityModel: WebsocketConnectivityModel
|
||||
|
||||
mailModel: MailModel
|
||||
mailboxModel: MailboxModel
|
||||
|
||||
calendarModel(): Promise<CalendarModel>
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ import { FolderSystem } from "../../common/mail/FolderSystem.js"
|
|||
import { AssociationType, Cardinality, Type as TypeId, ValueType } from "../../common/EntityConstants.js"
|
||||
import { OutOfSyncError } from "../../common/error/OutOfSyncError.js"
|
||||
import { sql, SqlFragment } from "./Sql.js"
|
||||
import { isDraft, isSpamOrTrashFolder } from "../../common/CommonMailUtils.js"
|
||||
import { isDraft, isSpamOrTrashFolder } from "../../../../mail-app/mail/model/MailModel.js"
|
||||
|
||||
/**
|
||||
* this is the value of SQLITE_MAX_VARIABLE_NUMBER in sqlite3.c
|
||||
|
|
|
@ -41,7 +41,8 @@ import { MailFacade } from "../facades/lazy/MailFacade.js"
|
|||
import { containsEventOfType, EntityUpdateData } from "../../common/utils/EntityUpdateUtils.js"
|
||||
import { b64UserIdHash } from "./DbFacade.js"
|
||||
import { hasError } from "../../common/utils/ErrorUtils.js"
|
||||
import { getDisplayedSender, getMailBodyText, isDraft, MailAddressAndName } from "../../common/CommonMailUtils.js"
|
||||
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../common/CommonMailUtils.js"
|
||||
import { isDraft } from "../../../../mail-app/mail/model/MailModel.js"
|
||||
|
||||
export const INITIAL_MAIL_INDEX_INTERVAL_DAYS = 28
|
||||
const ENTITY_INDEXER_CHUNK = 20
|
||||
|
|
|
@ -3,7 +3,7 @@ import { CredentialEncryptionMode } from "../../misc/credentials/CredentialEncry
|
|||
|
||||
/** the single source of truth for this configuration */
|
||||
export const SUPPORTED_MODES = Object.freeze([CredentialEncryptionMode.DEVICE_LOCK, CredentialEncryptionMode.APP_PASSWORD] as const)
|
||||
export type DesktopCredentialsMode = (typeof SUPPORTED_MODES)[number]
|
||||
export type DesktopCredentialsMode = typeof SUPPORTED_MODES[number]
|
||||
|
||||
export function assertSupportedEncryptionMode(encryptionMode: DesktopCredentialsMode) {
|
||||
assert(SUPPORTED_MODES.includes(encryptionMode), `should not use unsupported encryption mode ${encryptionMode}`)
|
||||
|
|
|
@ -17,8 +17,8 @@ import { getFileBaseName, getFileExtension, isTutanotaFile } from "../api/common
|
|||
import { getSafeAreaInsetBottom } from "./HtmlUtils.js"
|
||||
import { hasError } from "../api/common/utils/ErrorUtils.js"
|
||||
import { BubbleButton, bubbleButtonHeight, bubbleButtonPadding } from "./base/buttons/BubbleButton.js"
|
||||
import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js"
|
||||
import { BootIcons } from "./base/icons/BootIcons.js"
|
||||
import { CALENDAR_MIME_TYPE, VCARD_MIME_TYPES } from "../file/FileController.js"
|
||||
|
||||
export enum AttachmentType {
|
||||
GENERIC,
|
||||
|
|
|
@ -152,7 +152,7 @@ export class PostLoginActions implements PostLoginAction {
|
|||
|
||||
if (!isAdminClient()) {
|
||||
// If it failed during the partial login due to missing cache entries we will give it another spin here. If it didn't fail then it's just a noop
|
||||
await locator.mailModel.init()
|
||||
await locator.mailboxModel.init()
|
||||
const calendarModel = await locator.calendarModel()
|
||||
await calendarModel.init()
|
||||
await this.remindActiveOutOfOfficeNotification()
|
||||
|
|
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 { CustomerPropertiesTypeRef } from "../api/entities/sys/TypeRefs.js"
|
||||
import { isMailAddress } from "../misc/FormatValidator.js"
|
||||
import { MailboxDetail, MailModel } from "./MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "./MailboxModel.js"
|
||||
import { ContactModel } from "../contactsFunctionality/ContactModel.js"
|
||||
import { getContactDisplayName } from "../contactsFunctionality/ContactUtils.js"
|
||||
import { getMailBodyText } from "../api/common/CommonMailUtils.js"
|
||||
|
@ -152,13 +152,14 @@ export class SendMailModel {
|
|||
public readonly mailFacade: MailFacade,
|
||||
public readonly entity: EntityClient,
|
||||
public readonly logins: LoginController,
|
||||
public readonly mailModel: MailModel,
|
||||
public readonly mailboxModel: MailboxModel,
|
||||
public readonly contactModel: ContactModel,
|
||||
private readonly eventController: EventController,
|
||||
public readonly mailboxDetails: MailboxDetail,
|
||||
private readonly recipientsModel: RecipientsModel,
|
||||
private readonly dateProvider: DateProvider,
|
||||
private mailboxProperties: MailboxProperties,
|
||||
private readonly needNewDraft: (mail: Mail) => Promise<boolean>,
|
||||
) {
|
||||
const userProps = logins.getUserController().props
|
||||
this.senderAddress = this.getDefaultSender()
|
||||
|
@ -885,7 +886,7 @@ export class SendMailModel {
|
|||
}).html
|
||||
|
||||
this.draft =
|
||||
this.draft == null || (await this.isMailInTrashOrSpam(this.draft))
|
||||
this.draft == null || (await this.needNewDraft(this.draft))
|
||||
? await this.createDraft(body, attachments, mailMethod)
|
||||
: await this.updateDraft(body, attachments, this.draft)
|
||||
|
||||
|
@ -915,12 +916,6 @@ export class SendMailModel {
|
|||
}
|
||||
}
|
||||
|
||||
private async isMailInTrashOrSpam(draft: Mail): Promise<boolean> {
|
||||
const folders = await this.mailModel.getMailboxFolders(draft)
|
||||
const mailFolder = folders?.getFolderByMail(draft)
|
||||
return !!mailFolder && (mailFolder.folderType === MailSetKind.TRASH || mailFolder.folderType === MailSetKind.SPAM)
|
||||
}
|
||||
|
||||
private sendApprovalMail(body: string): Promise<unknown> {
|
||||
const listId = "---------c--"
|
||||
const m = createApprovalMail({
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
TutanotaProperties,
|
||||
} from "../api/entities/tutanota/TypeRefs.js"
|
||||
import { fullNameToFirstAndLastName, mailAddressToFirstAndLastName } from "../misc/parsing/MailAddressParser.js"
|
||||
import { assertNotNull, contains, endsWith, first, isNotEmpty, neverNull } from "@tutao/tutanota-utils"
|
||||
import { assertNotNull, contains, neverNull } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
ContactAddressType,
|
||||
ConversationType,
|
||||
|
@ -33,17 +33,17 @@ import { getEnabledMailAddressesForGroupInfo, getGroupInfoDisplayName } from "..
|
|||
import { lang, Language, TranslationKey } from "../misc/LanguageViewModel.js"
|
||||
import { AllIcons } from "../gui/base/Icon.js"
|
||||
import { Icons } from "../gui/base/icons/Icons.js"
|
||||
import { MailboxDetail, MailModel } from "./MailModel.js"
|
||||
import { MailboxDetail } from "./MailboxModel.js"
|
||||
import { LoginController } from "../api/main/LoginController.js"
|
||||
import { EntityClient } from "../api/common/EntityClient.js"
|
||||
import { getListId, isSameId } from "../api/common/utils/EntityUtils.js"
|
||||
import type { FolderSystem, IndentedFolder } from "../api/common/mail/FolderSystem.js"
|
||||
import type { FolderSystem } from "../api/common/mail/FolderSystem.js"
|
||||
import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js"
|
||||
import { ListFilter } from "../misc/ListModel.js"
|
||||
import { FontIcons } from "../gui/base/icons/FontIcons.js"
|
||||
import { ProgrammingError } from "../api/common/error/ProgrammingError.js"
|
||||
import { Attachment } from "./SendMailModel.js"
|
||||
import { getDisplayedSender, isDraft, isSubfolderOfType } from "../api/common/CommonMailUtils.js"
|
||||
import { getDisplayedSender } from "../api/common/CommonMailUtils.js"
|
||||
import { isDraft } from "../../mail-app/mail/model/MailModel.js"
|
||||
|
||||
assertMainOrNode()
|
||||
export const LINE_BREAK = "<br>"
|
||||
|
@ -359,26 +359,6 @@ export enum RecipientField {
|
|||
|
||||
export type FolderInfo = { level: number; folder: MailFolder }
|
||||
|
||||
export async function getMoveTargetFolderSystems(model: MailModel, mails: readonly Mail[]): Promise<Array<FolderInfo>> {
|
||||
const firstMail = first(mails)
|
||||
if (firstMail == null) return []
|
||||
|
||||
const mailboxDetails = await model.getMailboxDetailsForMail(firstMail)
|
||||
if (mailboxDetails == null) {
|
||||
return []
|
||||
}
|
||||
const folderSystem = mailboxDetails.folders
|
||||
|
||||
return folderSystem.getIndentedList().filter((f: IndentedFolder) => {
|
||||
if (f.folder.isMailSet && isNotEmpty(firstMail.sets)) {
|
||||
const folderId = firstMail.sets[0]
|
||||
return !isSameId(f.folder._id, folderId)
|
||||
} else {
|
||||
return f.folder.mails !== getListId(firstMail)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const MAX_FOLDER_INDENT_LEVEL = 10
|
||||
|
||||
export function getIndentedFolderNameForDropdown(folderInfo: FolderInfo) {
|
||||
|
@ -461,29 +441,7 @@ export function isTutanotaMailAddress(mailAddress: string): boolean {
|
|||
return TUTANOTA_MAIL_ADDRESS_DOMAINS.some((tutaDomain) => mailAddress.endsWith("@" + tutaDomain))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a system folder of the specified type and unwraps it.
|
||||
* Some system folders don't exist in some cases, e.g. spam or archive for external mailboxes!
|
||||
*
|
||||
* Use with caution.
|
||||
*/
|
||||
export function assertSystemFolderOfType(system: FolderSystem, type: Omit<MailSetKind, MailSetKind.CUSTOM>): MailFolder {
|
||||
return assertNotNull(system.getSystemFolderByType(type), "System folder of type does not exist!")
|
||||
}
|
||||
|
||||
export function isOfTypeOrSubfolderOf(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
|
||||
return folder.folderType === type || isSubfolderOfType(system, folder, type)
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: DOES NOT VERIFY IF THE MESSAGE IS AUTHENTIC - DO NOT USE THIS OUTSIDE OF THIS FILE OR FOR TESTING
|
||||
* @VisibleForTesting
|
||||
*/
|
||||
export function isTutanotaTeamAddress(address: string): boolean {
|
||||
return endsWith(address, "@tutao.de") || address === "no-reply@tutanota.de"
|
||||
}
|
||||
|
||||
function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mail): boolean {
|
||||
export function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mail): boolean {
|
||||
switch (encryptionAuthStatus) {
|
||||
// emails before tuta-crypt had no encryptionAuthStatus
|
||||
case null:
|
||||
|
@ -500,19 +458,6 @@ function hasValidEncryptionAuthForTeamOrSystemMail({ encryptionAuthStatus }: Mai
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a tutao team member email or a system notification
|
||||
*/
|
||||
export function isTutanotaTeamMail(mail: Mail): boolean {
|
||||
const { confidential, sender, state } = mail
|
||||
return (
|
||||
confidential &&
|
||||
state === MailState.RECEIVED &&
|
||||
hasValidEncryptionAuthForTeamOrSystemMail(mail) &&
|
||||
(sender.address === SYSTEM_GROUP_MAIL_ADDRESS || isTutanotaTeamAddress(sender.address))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a system notification?
|
||||
*/
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { MailboxDetail } from "../../mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../mailFunctionality/MailboxModel.js"
|
||||
import type { LoginController } from "../../api/main/LoginController"
|
||||
import { assertMainOrNode } from "../../api/common/Env"
|
||||
import { PartialRecipient } from "../../api/common/recipients/Recipient"
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import m from "mithril"
|
||||
import { locator } from "../../api/main/CommonLocator"
|
||||
import { MailSetKind } from "../../api/common/TutanotaConstants.js"
|
||||
|
||||
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js"
|
||||
import { assertSystemFolderOfType } from "../../../mail-app/mail/model/MailModel.js"
|
||||
import { mailLocator } from "../../../mail-app/mailLocator.js"
|
||||
import { assertNotNull } from "@tutao/tutanota-utils"
|
||||
import { getElementId } from "../../api/common/utils/EntityUtils.js"
|
||||
|
||||
export async function openMailbox(userId: Id, mailAddress: string, requestedPath: string | null) {
|
||||
if (locator.logins.isUserLoggedIn() && locator.logins.getUserController().user._id === userId) {
|
||||
if (!requestedPath) {
|
||||
const [mailboxDetail] = await locator.mailModel.getMailboxDetails()
|
||||
const inbox = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX)
|
||||
const [mailboxDetail] = await locator.mailboxModel.getMailboxDetails()
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
const inbox = assertSystemFolderOfType(folders, MailSetKind.INBOX)
|
||||
m.route.set("/mail/" + getElementId(inbox))
|
||||
} else {
|
||||
m.route.set("/mail" + requestedPath)
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Dialog } from "../../gui/base/Dialog.js"
|
|||
import { AttachmentType, getAttachmentType } from "../../gui/AttachmentBubble.js"
|
||||
import { showRequestPasswordDialog } from "../../misc/passwords/PasswordRequestDialog.js"
|
||||
import { LoginController } from "../../api/main/LoginController.js"
|
||||
import { MailModel } from "../../mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../mailFunctionality/MailboxModel.js"
|
||||
import { UsageTestController } from "@tutao/tutanota-usagetests"
|
||||
import { NativeFileApp } from "../common/FileApp.js"
|
||||
import { NativePushServiceApp } from "./NativePushServiceApp.js"
|
||||
|
@ -18,11 +18,13 @@ import { AppType } from "../../misc/ClientConstants.js"
|
|||
export class WebCommonNativeFacade implements CommonNativeFacade {
|
||||
constructor(
|
||||
private readonly logins: LoginController,
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly usageTestController: UsageTestController,
|
||||
private readonly fileApp: lazyAsync<NativeFileApp>,
|
||||
private readonly pushService: lazyAsync<NativePushServiceApp>,
|
||||
private readonly fileImportHandler: (filesUris: ReadonlyArray<string>) => unknown,
|
||||
readonly openMailBox: (userId: string, address: string, requestedPath: string | null) => Promise<void>,
|
||||
readonly openCalendar: (userId: string) => Promise<void>,
|
||||
private readonly appType: AppType,
|
||||
) {}
|
||||
|
||||
|
@ -46,7 +48,7 @@ export class WebCommonNativeFacade implements CommonNativeFacade {
|
|||
const { newMailEditorFromTemplate, newMailtoUrlMailEditor } = await import("../../../mail-app/mail/editor/MailEditor.js")
|
||||
const signatureModule = await import("../../../mail-app/mail/signature/Signature")
|
||||
await this.logins.waitForPartialLogin()
|
||||
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
|
||||
let editor
|
||||
|
||||
try {
|
||||
|
@ -130,16 +132,6 @@ export class WebCommonNativeFacade implements CommonNativeFacade {
|
|||
await pushService.reRegister()
|
||||
}
|
||||
|
||||
async openCalendar(userId: string): Promise<void> {
|
||||
const { openCalendar } = await import("./OpenMailboxHandler.js")
|
||||
return openCalendar(userId)
|
||||
}
|
||||
|
||||
async openMailBox(userId: string, address: string, requestedPath: string | null): Promise<void> {
|
||||
const { openMailbox } = await import("./OpenMailboxHandler.js")
|
||||
return openMailbox(userId, address, requestedPath)
|
||||
}
|
||||
|
||||
async showAlertDialog(translationKey: string): Promise<void> {
|
||||
const { Dialog } = await import("../../gui/base/Dialog.js")
|
||||
return Dialog.message(translationKey as TranslationKey)
|
||||
|
|
|
@ -7,12 +7,11 @@ import { CloseEventBusOption, MailSetKind, SECOND_MS } from "../../api/common/Tu
|
|||
import { MobileFacade } from "../common/generatedipc/MobileFacade.js"
|
||||
import { styles } from "../../gui/styles"
|
||||
import { WebsocketConnectivityModel } from "../../misc/WebsocketConnectivityModel.js"
|
||||
import { MailModel } from "../../mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../mailFunctionality/MailboxModel.js"
|
||||
import { TopLevelView } from "../../../TopLevelView.js"
|
||||
import stream from "mithril/stream"
|
||||
import { assertSystemFolderOfType } from "../../mailFunctionality/SharedMailUtils.js"
|
||||
import { assertSystemFolderOfType } from "../../../mail-app/mail/model/MailModel.js"
|
||||
import { CalendarViewType } from "../../api/common/utils/CommonCalendarUtils.js"
|
||||
import { getElementId } from "../../api/common/utils/EntityUtils.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -27,8 +26,9 @@ export class WebMobileFacade implements MobileFacade {
|
|||
|
||||
constructor(
|
||||
private readonly connectivityModel: WebsocketConnectivityModel,
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly baseViewPrefix: string,
|
||||
private readonly mailBackNewRoute?: (currentRoute: string) => Promise<string | null>,
|
||||
) {}
|
||||
|
||||
public getIsAppVisible(): stream<boolean> {
|
||||
|
@ -86,28 +86,13 @@ export class WebMobileFacade implements MobileFacade {
|
|||
m.route.set(this.baseViewPrefix)
|
||||
return true
|
||||
} else if (viewSlider && viewSlider.isFirstBackgroundColumnFocused()) {
|
||||
// If the first background column is focused in mail view (showing a folder), move to inbox.
|
||||
// If in inbox already, quit
|
||||
if (m.route.get().startsWith(MAIL_PREFIX)) {
|
||||
const parts = m.route
|
||||
.get()
|
||||
.split("/")
|
||||
.filter((part) => part !== "")
|
||||
|
||||
if (parts.length > 1) {
|
||||
const selectedMailFolderId = parts[1]
|
||||
const [mailboxDetail] = await this.mailModel.getMailboxDetails()
|
||||
const inboxMailFolderId = getElementId(assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX))
|
||||
|
||||
if (inboxMailFolderId !== selectedMailFolderId) {
|
||||
m.route.set(MAIL_PREFIX + "/" + inboxMailFolderId)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
if (currentRoute.startsWith(MAIL_PREFIX) && this.mailBackNewRoute) {
|
||||
const newRoute = await this.mailBackNewRoute(currentRoute)
|
||||
if (newRoute) {
|
||||
m.route.set(newRoute)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
|
|
|
@ -73,7 +73,7 @@ export class AboutDialog implements Component<AboutDialogAttrs> {
|
|||
async _sendDeviceLogs(): Promise<void> {
|
||||
const timestamp = new Date()
|
||||
const attachments = await getLogAttachments(timestamp)
|
||||
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
|
||||
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
|
||||
let { message, type, client } = clientInfoString(timestamp, true)
|
||||
message = message
|
||||
.split("\n")
|
||||
|
|
|
@ -8,7 +8,7 @@ import { PartialRecipient, Recipients } from "../api/common/recipients/Recipient
|
|||
import { getDefaultSender, getEnabledMailAddressesWithUser, getMailAddressDisplayText, getSenderNameForUser } from "../mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
export function sendShareNotificationEmail(sharedGroupInfo: GroupInfo, recipients: Array<PartialRecipient>, texts: GroupSharingTexts) {
|
||||
locator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
|
||||
locator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
|
||||
const senderMailAddress = getDefaultSender(locator.logins, mailboxDetails)
|
||||
const userName = getSenderNameForUser(mailboxDetails, locator.logins.getUserController())
|
||||
// Sending notifications as bcc so that invited people don't see each other
|
||||
|
@ -88,7 +88,7 @@ function _sendNotificationEmail(recipients: Recipients, subject: string, body: s
|
|||
allowRelativeLinks: false,
|
||||
usePlaceholderForInlineImages: false,
|
||||
}).html
|
||||
locator.mailModel.getUserMailboxDetails().then(async (mailboxDetails) => {
|
||||
locator.mailboxModel.getUserMailboxDetails().then(async (mailboxDetails) => {
|
||||
const sender = getEnabledMailAddressesWithUser(mailboxDetails, locator.logins.getUserController().userGroupInfo).includes(senderMailAddress)
|
||||
? senderMailAddress
|
||||
: getDefaultSender(locator.logins, mailboxDetails)
|
||||
|
@ -96,7 +96,7 @@ function _sendNotificationEmail(recipients: Recipients, subject: string, body: s
|
|||
const confirm = () => Promise.resolve(true)
|
||||
|
||||
const wait = showProgressDialog
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const model = await locator.sendMailModel(mailboxDetails, mailboxProperties)
|
||||
await model.initWithTemplate(recipients, subject, sanitizedBody, [], true, sender)
|
||||
await model.send(MailMethod.NONE, confirm, wait, "tooManyMailsAuto_msg")
|
||||
|
|
|
@ -930,7 +930,7 @@ export class ContactView extends BaseTopLevelView implements TopLevelView<Contac
|
|||
}
|
||||
|
||||
export function writeMail(to: PartialRecipient, subject: string = ""): Promise<unknown> {
|
||||
return locator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
|
||||
return locator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
|
||||
return newMailEditorFromTemplate(
|
||||
mailboxDetails,
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Editor, ImagePasteEvent } from "../../../common/gui/editor/Editor"
|
|||
import type { Attachment, InitAsResponseArgs, SendMailModel } from "../../../common/mailFunctionality/SendMailModel.js"
|
||||
import { Dialog } from "../../../common/gui/base/Dialog"
|
||||
import { InfoLink, lang } from "../../../common/misc/LanguageViewModel"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { checkApprovalStatus } from "../../../common/misc/LoginUtils"
|
||||
import { locator } from "../../../common/api/main/CommonLocator"
|
||||
import {
|
||||
|
@ -1156,7 +1156,7 @@ export async function newMailEditorFromTemplate(
|
|||
senderMailAddress?: string,
|
||||
initialChangedState?: boolean,
|
||||
): Promise<Dialog> {
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
return locator
|
||||
.sendMailModel(mailboxDetails, mailboxProperties)
|
||||
.then((model) => model.initWithTemplate(recipients, subject, bodyText, attachments, confidential, senderMailAddress, initialChangedState))
|
||||
|
@ -1261,7 +1261,7 @@ export async function writeGiftCardMail(link: string, svg: SVGElement, mailboxDe
|
|||
async function getMailboxDetailsAndProperties(
|
||||
mailboxDetails: MailboxDetail | null | undefined,
|
||||
): Promise<{ mailboxDetails: MailboxDetail; mailboxProperties: MailboxProperties }> {
|
||||
mailboxDetails = mailboxDetails ?? (await locator.mailModel.getUserMailboxDetails())
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
mailboxDetails = mailboxDetails ?? (await locator.mailboxModel.getUserMailboxDetails())
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
return { mailboxDetails, mailboxProperties }
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { InboxRuleType, MailSetKind, MAX_NBR_MOVE_DELETE_MAIL_SERVICE } from "..
|
|||
import { isDomainName, isRegularExpression } from "../../../common/misc/FormatValidator"
|
||||
import { assertNotNull, asyncFind, debounce, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
|
||||
import type { SelectorItemList } from "../../../common/gui/base/DropDownSelector.js"
|
||||
import { elementIdPart, isSameId } from "../../../common/api/common/utils/EntityUtils"
|
||||
|
@ -13,6 +13,8 @@ import { MailFacade } from "../../../common/api/worker/facades/lazy/MailFacade.j
|
|||
import { LoginController } from "../../../common/api/main/LoginController.js"
|
||||
import { getMailHeaders } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { throttle } from "@tutao/tutanota-utils/dist/Utils.js"
|
||||
import { assertSystemFolderOfType } from "./MailModel.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
assertMainOrNode()
|
||||
const moveMailDataPerFolder: MoveMailData[] = []
|
||||
|
@ -99,14 +101,21 @@ export class InboxRuleHandler {
|
|||
* @returns true if a rule matches otherwise false
|
||||
*/
|
||||
async findAndApplyMatchingRule(mailboxDetail: MailboxDetail, mail: Mail, applyRulesOnServer: boolean): Promise<{ folder: MailFolder; mail: Mail } | null> {
|
||||
if (mail._errors || !mail.unread || !isInboxFolder(mailboxDetail, mail) || !this.logins.getUserController().isPremiumAccount()) {
|
||||
if (
|
||||
mail._errors ||
|
||||
!mail.unread ||
|
||||
!isInboxFolder(mailboxDetail, mail) ||
|
||||
!this.logins.getUserController().isPremiumAccount() ||
|
||||
mailboxDetail.mailbox.folders == null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const inboxRule = await _findMatchingRule(this.mailFacade, mail, this.logins.getUserController().props.inboxRules)
|
||||
if (inboxRule) {
|
||||
let inboxFolder = assertNotNull(mailboxDetail.folders.getSystemFolderByType(MailSetKind.INBOX))
|
||||
let targetFolder = mailboxDetail.folders.getFolderById(elementIdPart(inboxRule.targetFolder))
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
let inboxFolder = assertNotNull(folders.getSystemFolderByType(MailSetKind.INBOX))
|
||||
let targetFolder = folders.getFolderById(elementIdPart(inboxRule.targetFolder))
|
||||
|
||||
if (targetFolder && targetFolder.folderType !== MailSetKind.INBOX) {
|
||||
if (applyRulesOnServer) {
|
||||
|
@ -225,6 +234,7 @@ function _checkEmailAddresses(mailAddresses: string[], inboxRule: InboxRule): bo
|
|||
}
|
||||
|
||||
export function isInboxFolder(mailboxDetail: MailboxDetail, mail: Mail): boolean {
|
||||
const mailFolder = mailboxDetail.folders.getFolderByMail(mail)
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
const mailFolder = folders.getFolderByMail(mail)
|
||||
return mailFolder?.folderType === MailSetKind.INBOX
|
||||
}
|
||||
|
|
|
@ -1,30 +1,17 @@
|
|||
import Stream from "mithril/stream"
|
||||
import stream from "mithril/stream"
|
||||
import { MailboxCounters, MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { FolderSystem, type IndentedFolder } from "../../../common/api/common/mail/FolderSystem.js"
|
||||
import { assertNotNull, first, groupBy, isNotEmpty, lazyMemoized, neverNull, noOp, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
|
||||
import {
|
||||
createMailAddressProperties,
|
||||
createMailboxProperties,
|
||||
Mail,
|
||||
MailBox,
|
||||
MailboxGroupRoot,
|
||||
MailboxGroupRootTypeRef,
|
||||
MailboxProperties,
|
||||
MailboxPropertiesTypeRef,
|
||||
MailBoxTypeRef,
|
||||
MailFolder,
|
||||
MailFolderTypeRef,
|
||||
MailSetEntryTypeRef,
|
||||
MailTypeRef,
|
||||
} from "../api/entities/tutanota/TypeRefs.js"
|
||||
import { Group, GroupInfo, GroupInfoTypeRef, GroupMembership, GroupTypeRef, WebsocketCounterData } from "../api/entities/sys/TypeRefs.js"
|
||||
import { FolderSystem } from "../api/common/mail/FolderSystem.js"
|
||||
import Stream from "mithril/stream"
|
||||
import stream from "mithril/stream"
|
||||
import { Notifications, NotificationType } from "../gui/Notifications.js"
|
||||
import { EventController } from "../api/main/EventController.js"
|
||||
import { MailFacade } from "../api/worker/facades/lazy/MailFacade.js"
|
||||
import { EntityClient } from "../api/common/EntityClient.js"
|
||||
import { LoginController } from "../api/main/LoginController.js"
|
||||
import { WebsocketConnectivityModel } from "../misc/WebsocketConnectivityModel.js"
|
||||
import { InboxRuleHandler } from "../../mail-app/mail/model/InboxRuleHandler.js"
|
||||
import { assertNotNull, groupBy, isNotEmpty, lazyMemoized, neverNull, noOp, ofClass, promiseMap, splitInChunks } from "@tutao/tutanota-utils"
|
||||
} from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import {
|
||||
FeatureType,
|
||||
MailReportType,
|
||||
|
@ -32,138 +19,120 @@ import {
|
|||
MAX_NBR_MOVE_DELETE_MAIL_SERVICE,
|
||||
OperationType,
|
||||
ReportMovedMailsType,
|
||||
} from "../api/common/TutanotaConstants.js"
|
||||
import { assertSystemFolderOfType, getEnabledMailAddressesWithUser } from "./SharedMailUtils.js"
|
||||
import { LockedError, NotFoundError, PreconditionFailedError } from "../api/common/error/RestError.js"
|
||||
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../api/common/utils/EntityUtils.js"
|
||||
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../api/common/utils/EntityUpdateUtils.js"
|
||||
} from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { CUSTOM_MIN_ID, elementIdPart, GENERATED_MAX_ID, getElementId, getListId, isSameId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { FolderInfo } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { containsEventOfType, EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
import m from "mithril"
|
||||
import { lang } from "../misc/LanguageViewModel.js"
|
||||
import { ProgrammingError } from "../api/common/error/ProgrammingError.js"
|
||||
import { UserError } from "../api/main/UserError.js"
|
||||
import { isSpamOrTrashFolder } from "../api/common/CommonMailUtils.js"
|
||||
|
||||
export type MailboxDetail = {
|
||||
mailbox: MailBox
|
||||
folders: FolderSystem
|
||||
mailGroupInfo: GroupInfo
|
||||
mailGroup: Group
|
||||
mailboxGroupRoot: MailboxGroupRoot
|
||||
}
|
||||
|
||||
export type MailboxCounters = Record<Id, Record<string, number>>
|
||||
import { WebsocketCounterData } from "../../../common/api/entities/sys/TypeRefs.js"
|
||||
import { Notifications, NotificationType } from "../../../common/gui/Notifications.js"
|
||||
import { lang } from "../../../common/misc/LanguageViewModel.js"
|
||||
import { ProgrammingError } from "../../../common/api/common/error/ProgrammingError.js"
|
||||
import { LockedError, NotFoundError, PreconditionFailedError } from "../../../common/api/common/error/RestError.js"
|
||||
import { UserError } from "../../../common/api/main/UserError.js"
|
||||
import { EventController } from "../../../common/api/main/EventController.js"
|
||||
import { InboxRuleHandler } from "./InboxRuleHandler.js"
|
||||
import { WebsocketConnectivityModel } from "../../../common/misc/WebsocketConnectivityModel.js"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { LoginController } from "../../../common/api/main/LoginController.js"
|
||||
import { MailFacade } from "../../../common/api/worker/facades/lazy/MailFacade.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
export class MailModel {
|
||||
/** Empty stream until init() is finished, exposed mostly for map()-ing, use getMailboxDetails to get a promise */
|
||||
readonly mailboxDetails: Stream<MailboxDetail[]> = stream()
|
||||
readonly mailboxCounters: Stream<MailboxCounters> = stream({})
|
||||
private initialization: Promise<void> | null = null
|
||||
/**
|
||||
* Map from MailboxGroupRoot id to MailboxProperties
|
||||
* A way to avoid race conditions in case we try to create mailbox properties from multiple places.
|
||||
*
|
||||
*/
|
||||
private mailboxPropertiesPromises: Map<Id, Promise<MailboxProperties>> = new Map()
|
||||
readonly folders: Stream<Record<Id, FolderSystem>> = stream()
|
||||
|
||||
constructor(
|
||||
private readonly notifications: Notifications,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly eventController: EventController,
|
||||
private readonly mailFacade: MailFacade,
|
||||
private readonly entityClient: EntityClient,
|
||||
private readonly logins: LoginController,
|
||||
private readonly mailFacade: MailFacade,
|
||||
private readonly connectivityModel: WebsocketConnectivityModel | null,
|
||||
private readonly inboxRuleHandler: InboxRuleHandler | null,
|
||||
) {}
|
||||
|
||||
// only init listeners once
|
||||
private readonly initListeners = lazyMemoized(() => {
|
||||
this.eventController.addEntityListener((updates, eventOwnerGroupId) => this.entityEventsReceived(updates, eventOwnerGroupId))
|
||||
this.eventController.addEntityListener((updates) => this.entityEventsReceived(updates))
|
||||
|
||||
this.eventController.getCountersStream().map((update) => {
|
||||
this._mailboxCountersUpdates(update)
|
||||
})
|
||||
})
|
||||
|
||||
init(): Promise<void> {
|
||||
// if we are in the process of loading do not start another one in parallel
|
||||
if (this.initialization) {
|
||||
return this.initialization
|
||||
}
|
||||
async init(): Promise<void> {
|
||||
this.initListeners()
|
||||
|
||||
return this._init()
|
||||
}
|
||||
const mailboxDetails = this.mailboxModel.mailboxDetails() || []
|
||||
|
||||
private _init(): Promise<void> {
|
||||
const mailGroupMemberships = this.logins.getUserController().getMailGroupMemberships()
|
||||
const mailBoxDetailsPromises = mailGroupMemberships.map((m) => this.mailboxDetailsFromMembership(m))
|
||||
this.initialization = Promise.all(mailBoxDetailsPromises).then((details) => {
|
||||
this.mailboxDetails(details)
|
||||
})
|
||||
return this.initialization.catch((e) => {
|
||||
console.warn("mail model initialization failed!", e)
|
||||
this.initialization = null
|
||||
throw e
|
||||
})
|
||||
}
|
||||
let tempFolders: Record<Id, FolderSystem> = {}
|
||||
|
||||
/**
|
||||
* load mailbox details from a mailgroup membership
|
||||
*/
|
||||
private async mailboxDetailsFromMembership(membership: GroupMembership): Promise<MailboxDetail> {
|
||||
const [mailboxGroupRoot, mailGroupInfo, mailGroup] = await Promise.all([
|
||||
this.entityClient.load(MailboxGroupRootTypeRef, membership.group),
|
||||
this.entityClient.load(GroupInfoTypeRef, membership.groupInfo),
|
||||
this.entityClient.load(GroupTypeRef, membership.group),
|
||||
])
|
||||
const mailbox = await this.entityClient.load(MailBoxTypeRef, mailboxGroupRoot.mailbox)
|
||||
const folders = await this.loadFolders(neverNull(mailbox.folders).folders)
|
||||
return {
|
||||
mailbox,
|
||||
folders: new FolderSystem(folders),
|
||||
mailGroupInfo,
|
||||
mailGroup,
|
||||
mailboxGroupRoot,
|
||||
for (let detail of mailboxDetails) {
|
||||
if (detail.mailbox.folders) {
|
||||
const detailFolders = await this.mailboxModel.loadFolders(neverNull(detail.mailbox.folders).folders)
|
||||
tempFolders[detail.mailbox.folders._id] = new FolderSystem(detailFolders)
|
||||
}
|
||||
}
|
||||
|
||||
this.folders(tempFolders)
|
||||
}
|
||||
|
||||
private loadFolders(folderListId: Id): Promise<MailFolder[]> {
|
||||
return this.entityClient.loadAll(MailFolderTypeRef, folderListId).then((folders) => {
|
||||
return folders.filter((f) => {
|
||||
// We do not show spam or archive for external users
|
||||
if (!this.logins.isInternalUserLoggedIn() && (f.folderType === MailSetKind.SPAM || f.folderType === MailSetKind.ARCHIVE)) {
|
||||
return false
|
||||
} else {
|
||||
return !(this.logins.isEnabled(FeatureType.InternalCommunication) && f.folderType === MailSetKind.SPAM)
|
||||
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>): Promise<void> {
|
||||
for (const update of updates) {
|
||||
if (isUpdateForTypeRef(MailFolderTypeRef, update)) {
|
||||
await this.init()
|
||||
m.redraw()
|
||||
} else if (
|
||||
isUpdateForTypeRef(MailTypeRef, update) &&
|
||||
update.operation === OperationType.CREATE &&
|
||||
!containsEventOfType(updates, OperationType.DELETE, update.instanceId)
|
||||
) {
|
||||
if (this.inboxRuleHandler && this.connectivityModel) {
|
||||
const mailId: IdTuple = [update.instanceListId, update.instanceId]
|
||||
try {
|
||||
const mail = await this.entityClient.load(MailTypeRef, mailId)
|
||||
const folder = this.getMailFolderForMail(mail)
|
||||
|
||||
if (folder && folder.folderType === MailSetKind.INBOX) {
|
||||
// If we don't find another delete operation on this email in the batch, then it should be a create operation,
|
||||
// otherwise it's a move
|
||||
await this.getMailboxDetailsForMail(mail)
|
||||
.then((mailboxDetail) => {
|
||||
// We only apply rules on server if we are the leader in case of incoming messages
|
||||
return (
|
||||
mailboxDetail &&
|
||||
this.inboxRuleHandler?.findAndApplyMatchingRule(
|
||||
mailboxDetail,
|
||||
mail,
|
||||
this.connectivityModel ? this.connectivityModel.isLeader() : false,
|
||||
)
|
||||
)
|
||||
})
|
||||
.then((newFolderAndMail) => {
|
||||
if (newFolderAndMail) {
|
||||
this._showNotification(newFolderAndMail.folder, newFolderAndMail.mail)
|
||||
} else {
|
||||
this._showNotification(folder, mail)
|
||||
}
|
||||
})
|
||||
.catch(noOp)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundError) {
|
||||
console.log(`Could not find updated mail ${JSON.stringify(mailId)}`)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of MailboxDetails that this user has access to from their memberships.
|
||||
*
|
||||
* Will wait for successful initialization.
|
||||
*/
|
||||
async getMailboxDetails(): Promise<Array<MailboxDetail>> {
|
||||
// If details are there, use them
|
||||
if (this.mailboxDetails()) {
|
||||
return this.mailboxDetails()
|
||||
} else {
|
||||
// If they are not there, trigger loading again (just in case) but do not fail and wait until we actually have the details.
|
||||
// This is so that the rest of the app is not in the broken state if details fail to load but is just waiting until the success.
|
||||
return new Promise((resolve) => {
|
||||
this.init()
|
||||
const end = this.mailboxDetails.map((details) => {
|
||||
resolve(details)
|
||||
end.end(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getMailboxDetailsForMail(mail: Mail): Promise<MailboxDetail | null> {
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
const mailboxDetails = await this.mailboxModel.getMailboxDetails()
|
||||
const detail = mailboxDetails.find((md) => md.folders.getFolderByMail(mail)) ?? null
|
||||
if (detail == null) {
|
||||
console.warn("Mailbox detail for mail does not exist", mail)
|
||||
|
@ -172,7 +141,7 @@ export class MailModel {
|
|||
}
|
||||
|
||||
async getMailboxDetailsForMailFolder(mailFolder: MailFolder): Promise<MailboxDetail | null> {
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
const mailboxDetails = await this.mailboxModel.getMailboxDetails()
|
||||
const detail = mailboxDetails.find((md) => md.folders.getFolderById(getElementId(mailFolder))) ?? null
|
||||
if (detail == null) {
|
||||
console.warn("Mailbox detail for mail folder does not exist", mailFolder)
|
||||
|
@ -180,29 +149,23 @@ export class MailModel {
|
|||
return detail
|
||||
}
|
||||
|
||||
async getMailboxDetailsForMailGroup(mailGroupId: Id): Promise<MailboxDetail> {
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
return assertNotNull(
|
||||
mailboxDetails.find((md) => mailGroupId === md.mailGroup._id),
|
||||
"Mailbox detail for mail group does not exist",
|
||||
)
|
||||
getMailboxFoldersForMail(mail: Mail): Promise<FolderSystem | null> {
|
||||
return this.getMailboxDetailsForMail(mail).then((md) => {
|
||||
if (md && md.mailbox.folders) {
|
||||
const folderStructures = this.folders()
|
||||
return folderStructures[md.mailbox.folders._id] ?? null
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
async getUserMailboxDetails(): Promise<MailboxDetail> {
|
||||
const userMailGroupMembership = this.logins.getUserController().getUserMailGroupMembership()
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
return assertNotNull(
|
||||
mailboxDetails.find((md) => md.mailGroup._id === userMailGroupMembership.group),
|
||||
"Mailbox detail for user does not exist",
|
||||
)
|
||||
}
|
||||
|
||||
async getMailboxFolders(mail: Mail): Promise<FolderSystem | null> {
|
||||
return this.getMailboxDetailsForMail(mail).then((md) => md && md.folders)
|
||||
getMailboxFoldersForId(foldersId: Id): FolderSystem {
|
||||
const folderStructures = this.folders()
|
||||
return folderStructures[foldersId]
|
||||
}
|
||||
|
||||
getMailFolderForMail(mail: Mail): MailFolder | null {
|
||||
const mailboxDetails = this.mailboxDetails() || []
|
||||
const mailboxDetails = this.mailboxModel.mailboxDetails() || []
|
||||
|
||||
let foundFolder: MailFolder | null = null
|
||||
for (let detail of mailboxDetails) {
|
||||
|
@ -217,27 +180,6 @@ export class MailModel {
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the given folder and all its descendants to the spam folder, reporting mails (if applicable) and removes any empty folders
|
||||
*/
|
||||
async sendFolderToSpam(folder: MailFolder): Promise<void> {
|
||||
const mailboxDetail = await this.getMailboxDetailsForMailFolder(folder)
|
||||
if (mailboxDetail == null) {
|
||||
return
|
||||
}
|
||||
|
||||
let deletedFolder = await this.removeAllEmpty(mailboxDetail, folder)
|
||||
if (!deletedFolder) {
|
||||
return this.mailFacade.updateMailFolderParent(folder, assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)._id)
|
||||
}
|
||||
}
|
||||
|
||||
async reportMails(reportType: MailReportType, mails: ReadonlyArray<Mail>): Promise<void> {
|
||||
for (const mail of mails) {
|
||||
await this.mailFacade.reportMail(mail, reportType).catch(ofClass(NotFoundError, (e) => console.log("mail to be reported not found", e)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finally move all given mails. Caller must ensure that mails are only from
|
||||
* * one folder (because we send one source folder)
|
||||
|
@ -284,27 +226,6 @@ export class MailModel {
|
|||
}
|
||||
}
|
||||
|
||||
isMovingMailsAllowed(): boolean {
|
||||
return this.logins.getUserController().isInternalUser()
|
||||
}
|
||||
|
||||
isExportingMailsAllowed(): boolean {
|
||||
return !this.logins.isEnabled(FeatureType.DisableMailExport)
|
||||
}
|
||||
|
||||
async markMails(mails: readonly Mail[], unread: boolean): Promise<void> {
|
||||
await promiseMap(
|
||||
mails,
|
||||
async (mail) => {
|
||||
if (mail.unread !== unread) {
|
||||
mail.unread = unread
|
||||
return this.entityClient.update(mail).catch(ofClass(NotFoundError, noOp)).catch(ofClass(LockedError, noOp))
|
||||
}
|
||||
},
|
||||
{ concurrency: 5 },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finally deletes the given mails if they are already in the trash or spam folders,
|
||||
* otherwise moves them to the trash folder.
|
||||
|
@ -357,71 +278,62 @@ export class MailModel {
|
|||
}
|
||||
}
|
||||
|
||||
async entityEventsReceived(updates: ReadonlyArray<EntityUpdateData>, eventOwnerGroupId: Id): Promise<void> {
|
||||
for (const update of updates) {
|
||||
if (isUpdateForTypeRef(MailFolderTypeRef, update)) {
|
||||
await this._init()
|
||||
m.redraw()
|
||||
} else if (isUpdateForTypeRef(GroupInfoTypeRef, update)) {
|
||||
if (update.operation === OperationType.UPDATE) {
|
||||
await this._init()
|
||||
m.redraw
|
||||
}
|
||||
} else if (this.logins.getUserController().isUpdateForLoggedInUserInstance(update, eventOwnerGroupId)) {
|
||||
let newMemberships = this.logins.getUserController().getMailGroupMemberships()
|
||||
const mailboxDetails = await this.getMailboxDetails()
|
||||
/**
|
||||
* Finally deletes all given mails. Caller must ensure that mails are only from one folder and the folder must allow final delete operation.
|
||||
*/
|
||||
private async finallyDeleteMails(mails: Mail[]): Promise<void> {
|
||||
if (!mails.length) return Promise.resolve()
|
||||
const mailFolder = neverNull(this.getMailFolderForMail(mails[0]))
|
||||
const mailIds = mails.map((m) => m._id)
|
||||
const mailChunks = splitInChunks(MAX_NBR_MOVE_DELETE_MAIL_SERVICE, mailIds)
|
||||
|
||||
if (newMemberships.length !== mailboxDetails.length) {
|
||||
await this._init()
|
||||
m.redraw()
|
||||
}
|
||||
} else if (
|
||||
isUpdateForTypeRef(MailTypeRef, update) &&
|
||||
update.operation === OperationType.CREATE &&
|
||||
!containsEventOfType(updates, OperationType.DELETE, update.instanceId)
|
||||
) {
|
||||
if (this.inboxRuleHandler && this.connectivityModel) {
|
||||
const mailId: IdTuple = [update.instanceListId, update.instanceId]
|
||||
try {
|
||||
const mail = await this.entityClient.load(MailTypeRef, mailId)
|
||||
const folder = this.getMailFolderForMail(mail)
|
||||
|
||||
if (folder && folder.folderType === MailSetKind.INBOX) {
|
||||
// If we don't find another delete operation on this email in the batch, then it should be a create operation,
|
||||
// otherwise it's a move
|
||||
await this.getMailboxDetailsForMail(mail)
|
||||
.then((mailboxDetail) => {
|
||||
// We only apply rules on server if we are the leader in case of incoming messages
|
||||
return (
|
||||
mailboxDetail &&
|
||||
this.inboxRuleHandler?.findAndApplyMatchingRule(
|
||||
mailboxDetail,
|
||||
mail,
|
||||
this.connectivityModel ? this.connectivityModel.isLeader() : false,
|
||||
)
|
||||
)
|
||||
})
|
||||
.then((newFolderAndMail) => {
|
||||
if (newFolderAndMail) {
|
||||
this._showNotification(newFolderAndMail.folder, newFolderAndMail.mail)
|
||||
} else {
|
||||
this._showNotification(folder, mail)
|
||||
}
|
||||
})
|
||||
.catch(noOp)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundError) {
|
||||
console.log(`Could not find updated mail ${JSON.stringify(mailId)}`)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const mailChunk of mailChunks) {
|
||||
await this.mailFacade.deleteMails(mailChunk, mailFolder._id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the given folder and all its descendants to the spam folder, reporting mails (if applicable) and removes any empty folders
|
||||
*/
|
||||
async sendFolderToSpam(folder: MailFolder): Promise<void> {
|
||||
const mailboxDetail = await this.getMailboxDetailsForMailFolder(folder)
|
||||
if (mailboxDetail == null) {
|
||||
return
|
||||
}
|
||||
|
||||
let deletedFolder = await this.removeAllEmpty(mailboxDetail, folder)
|
||||
if (!deletedFolder) {
|
||||
return this.mailFacade.updateMailFolderParent(folder, assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)._id)
|
||||
}
|
||||
}
|
||||
|
||||
async reportMails(reportType: MailReportType, mails: ReadonlyArray<Mail>): Promise<void> {
|
||||
for (const mail of mails) {
|
||||
await this.mailFacade.reportMail(mail, reportType).catch(ofClass(NotFoundError, (e) => console.log("mail to be reported not found", e)))
|
||||
}
|
||||
}
|
||||
|
||||
isMovingMailsAllowed(): boolean {
|
||||
return this.logins.getUserController().isInternalUser()
|
||||
}
|
||||
|
||||
isExportingMailsAllowed(): boolean {
|
||||
return !this.logins.isEnabled(FeatureType.DisableMailExport)
|
||||
}
|
||||
|
||||
async markMails(mails: readonly Mail[], unread: boolean): Promise<void> {
|
||||
await promiseMap(
|
||||
mails,
|
||||
async (mail) => {
|
||||
if (mail.unread !== unread) {
|
||||
mail.unread = unread
|
||||
return this.entityClient.update(mail).catch(ofClass(NotFoundError, noOp)).catch(ofClass(LockedError, noOp))
|
||||
}
|
||||
},
|
||||
{ concurrency: 5 },
|
||||
)
|
||||
}
|
||||
|
||||
_mailboxCountersUpdates(counters: WebsocketCounterData) {
|
||||
const normalized = this.mailboxCounters() || {}
|
||||
const group = normalized[counters.mailGroup] || {}
|
||||
|
@ -562,77 +474,80 @@ export class MailModel {
|
|||
await this.mailFacade.unsubscribe(mail._id, recipient, headers)
|
||||
}
|
||||
|
||||
async getMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
|
||||
// MailboxProperties is an encrypted instance that is created lazily. When we create it the reference is automatically written to the MailboxGroupRoot.
|
||||
// Unfortunately we will only get updated new MailboxGroupRoot with the next EntityUpdate.
|
||||
// To prevent parallel creation attempts we do two things:
|
||||
// - we save the loading promise to avoid calling setup() twice in parallel
|
||||
// - we set mailboxProperties reference manually (we could save the id elsewhere but it's easier this way)
|
||||
|
||||
// If we are already loading/creating, just return it to avoid races
|
||||
const existingPromise = this.mailboxPropertiesPromises.get(mailboxGroupRoot._id)
|
||||
if (existingPromise) {
|
||||
return existingPromise
|
||||
}
|
||||
|
||||
const promise: Promise<MailboxProperties> = this.loadOrCreateMailboxProperties(mailboxGroupRoot)
|
||||
this.mailboxPropertiesPromises.set(mailboxGroupRoot._id, promise)
|
||||
return promise.finally(() => this.mailboxPropertiesPromises.delete(mailboxGroupRoot._id))
|
||||
}
|
||||
|
||||
private async loadOrCreateMailboxProperties(mailboxGroupRoot: MailboxGroupRoot): Promise<MailboxProperties> {
|
||||
if (!mailboxGroupRoot.mailboxProperties) {
|
||||
mailboxGroupRoot.mailboxProperties = await this.entityClient
|
||||
.setup(
|
||||
null,
|
||||
createMailboxProperties({
|
||||
_ownerGroup: mailboxGroupRoot._ownerGroup ?? "",
|
||||
reportMovedMails: "0",
|
||||
mailAddressProperties: [],
|
||||
}),
|
||||
)
|
||||
.catch(
|
||||
ofClass(PreconditionFailedError, (e) => {
|
||||
// We try to prevent race conditions but they can still happen with multiple clients trying ot create mailboxProperties at the same time.
|
||||
// We send special precondition from the server with an existing id.
|
||||
if (e.data && e.data.startsWith("exists:")) {
|
||||
const existingId = e.data.substring("exists:".length)
|
||||
console.log("mailboxProperties already exists", existingId)
|
||||
return existingId
|
||||
} else {
|
||||
throw new ProgrammingError(`Could not create mailboxProperties, precondition: ${e.data}`)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
const mailboxProperties = await this.entityClient.load(MailboxPropertiesTypeRef, mailboxGroupRoot.mailboxProperties)
|
||||
if (mailboxProperties.mailAddressProperties.length === 0) {
|
||||
await this.migrateFromOldSenderName(mailboxGroupRoot, mailboxProperties)
|
||||
}
|
||||
return mailboxProperties
|
||||
}
|
||||
|
||||
/** If there was no sender name configured before take the user's name and assign it to all email addresses. */
|
||||
private async migrateFromOldSenderName(mailboxGroupRoot: MailboxGroupRoot, mailboxProperties: MailboxProperties) {
|
||||
const userGroupInfo = this.logins.getUserController().userGroupInfo
|
||||
const legacySenderName = userGroupInfo.name
|
||||
const mailboxDetails = await this.getMailboxDetailsForMailGroup(mailboxGroupRoot._id)
|
||||
const mailAddresses = getEnabledMailAddressesWithUser(mailboxDetails, userGroupInfo)
|
||||
for (const mailAddress of mailAddresses) {
|
||||
mailboxProperties.mailAddressProperties.push(
|
||||
createMailAddressProperties({
|
||||
mailAddress,
|
||||
senderName: legacySenderName,
|
||||
}),
|
||||
)
|
||||
}
|
||||
await this.entityClient.update(mailboxProperties)
|
||||
}
|
||||
|
||||
async saveReportMovedMails(mailboxGroupRoot: MailboxGroupRoot, reportMovedMails: ReportMovedMailsType): Promise<MailboxProperties> {
|
||||
const mailboxProperties = await this.loadOrCreateMailboxProperties(mailboxGroupRoot)
|
||||
const mailboxProperties = await this.mailboxModel.loadOrCreateMailboxProperties(mailboxGroupRoot)
|
||||
mailboxProperties.reportMovedMails = reportMovedMails
|
||||
await this.entityClient.update(mailboxProperties)
|
||||
return mailboxProperties
|
||||
}
|
||||
|
||||
async getMailboxFolders(mail: Mail): Promise<FolderSystem | null> {
|
||||
return this.getMailboxDetailsForMail(mail).then((md) => md && md.folders)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMoveTargetFolderSystems(foldersModel: MailModel, mails: readonly Mail[]): Promise<Array<FolderInfo>> {
|
||||
const firstMail = first(mails)
|
||||
if (firstMail == null) return []
|
||||
|
||||
const mailboxDetails = await foldersModel.getMailboxDetailsForMail(firstMail)
|
||||
if (mailboxDetails == null || mailboxDetails.mailbox.folders == null) {
|
||||
return []
|
||||
}
|
||||
const folderStructures = foldersModel.folders()
|
||||
const folderSystem = folderStructures[mailboxDetails.mailbox.folders._id]
|
||||
return folderSystem.getIndentedList().filter((f: IndentedFolder) => {
|
||||
if (f.folder.isMailSet && isNotEmpty(firstMail.sets)) {
|
||||
const folderId = firstMail.sets[0]
|
||||
return !isSameId(f.folder._id, folderId)
|
||||
} else {
|
||||
return f.folder.mails !== getListId(firstMail)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function isSubfolderOfType(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
|
||||
const systemFolder = system.getSystemFolderByType(type)
|
||||
return systemFolder != null && system.checkFolderForAncestor(folder, systemFolder._id)
|
||||
}
|
||||
|
||||
export function isDraft(mail: Mail): boolean {
|
||||
return mail.mailDetailsDraft != null
|
||||
}
|
||||
|
||||
export async function isMailInSpamOrTrash(mail: Mail): Promise<boolean> {
|
||||
const folders = await mailLocator.mailModel.getMailboxFoldersForMail(mail)
|
||||
const mailFolder = folders?.getFolderByMail(mail)
|
||||
if (folders && mailFolder) {
|
||||
return isSpamOrTrashFolder(folders, mailFolder)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if given folder is the {@link MailFolderType.SPAM} or {@link MailFolderType.TRASH} folder, or a descendant of those folders.
|
||||
*/
|
||||
export function isSpamOrTrashFolder(system: FolderSystem, folder: MailFolder): boolean {
|
||||
// not using isOfTypeOrSubfolderOf because checking the type first is cheaper
|
||||
return (
|
||||
folder.folderType === MailSetKind.TRASH ||
|
||||
folder.folderType === MailSetKind.SPAM ||
|
||||
isSubfolderOfType(system, folder, MailSetKind.TRASH) ||
|
||||
isSubfolderOfType(system, folder, MailSetKind.SPAM)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a system folder of the specified type and unwraps it.
|
||||
* Some system folders don't exist in some cases, e.g. spam or archive for external mailboxes!
|
||||
*
|
||||
* Use with caution.
|
||||
*/
|
||||
export function assertSystemFolderOfType(system: FolderSystem, type: Omit<MailSetKind, MailSetKind.CUSTOM>): MailFolder {
|
||||
return assertNotNull(system.getSystemFolderByType(type), "System folder of type does not exist!")
|
||||
}
|
||||
|
||||
export function isOfTypeOrSubfolderOf(system: FolderSystem, folder: MailFolder, type: MailSetKind): boolean {
|
||||
return folder.folderType === type || isSubfolderOfType(system, folder, type)
|
||||
}
|
|
@ -6,7 +6,7 @@ import { ButtonType } from "../../../common/gui/base/Button.js"
|
|||
import { isMailAddress } from "../../../common/misc/FormatValidator"
|
||||
import { UserError } from "../../../common/api/main/UserError"
|
||||
import { showUserError } from "../../../common/misc/ErrorHandlerImpl"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { Keys, MailMethod, TabIndex } from "../../../common/api/common/TutanotaConstants"
|
||||
import { progressIcon } from "../../../common/gui/base/Icon"
|
||||
import { Editor } from "../../../common/gui/editor/Editor"
|
||||
|
@ -29,7 +29,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
|
|||
}
|
||||
|
||||
async function send() {
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const body = pressRelease.bodyHtml()
|
||||
const subject = pressRelease.subject()
|
||||
let recipients
|
||||
|
@ -112,7 +112,7 @@ export function openPressReleaseEditor(mailboxDetails: MailboxDetail): void {
|
|||
const bodyWithGreeting = `<p>${recipient.greeting},</p>${body}`
|
||||
|
||||
try {
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const sendMailModel = await locator.sendMailModel(mailboxDetails, mailboxProperties)
|
||||
const model = await sendMailModel.initWithTemplate(
|
||||
{
|
||||
|
|
|
@ -8,10 +8,11 @@ import { LoadingStateTracker } from "../../../common/offline/LoadingState.js"
|
|||
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
|
||||
import { ConversationType, MailSetKind, MailState, OperationType } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError.js"
|
||||
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.js"
|
||||
import { isOfTypeOrSubfolderOf } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
import { isOfTypeOrSubfolderOf, MailModel } from "../model/MailModel.js"
|
||||
|
||||
export type MailViewerViewModelFactory = (options: CreateMailViewerOptions) => MailViewerViewModel
|
||||
|
||||
|
@ -257,7 +258,11 @@ export class ConversationViewModel {
|
|||
private async isInTrash(mail: Mail) {
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(mail)
|
||||
const mailFolder = this.mailModel.getMailFolderForMail(mail)
|
||||
return mailFolder && mailboxDetail && isOfTypeOrSubfolderOf(mailboxDetail.folders, mailFolder, MailSetKind.TRASH)
|
||||
if (mailFolder == null || mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
|
||||
return
|
||||
}
|
||||
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
return isOfTypeOrSubfolderOf(folders, mailFolder, MailSetKind.TRASH)
|
||||
}
|
||||
|
||||
conversationItems(): ReadonlyArray<ConversationItem> {
|
||||
|
|
|
@ -6,14 +6,17 @@ import { Dialog } from "../../../common/gui/base/Dialog.js"
|
|||
import { locator } from "../../../common/api/main/CommonLocator.js"
|
||||
import { LockedError } from "../../../common/api/common/error/RestError.js"
|
||||
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel.js"
|
||||
import { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
|
||||
import { elementIdPart, isSameId, listIdPart } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { reportMailsAutomatically } from "./MailReportDialog.js"
|
||||
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
|
||||
import { getFolderName, getIndentedFolderNameForDropdown, getPathToFolderString } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { isSpamOrTrashFolder } from "../model/MailModel.js"
|
||||
import { groupByAndMap } from "@tutao/tutanota-utils"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
import { assertNotNull } from "@tutao/tutanota-utils"
|
||||
import type { FolderSystem, IndentedFolder } from "../../../common/api/common/mail/FolderSystem.js"
|
||||
|
||||
/**
|
||||
* Dialog for Edit and Add folder are the same.
|
||||
|
@ -22,12 +25,13 @@ import { groupByAndMap } from "@tutao/tutanota-utils"
|
|||
export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedFolder: MailFolder | null = null, parentFolder: MailFolder | null = null) {
|
||||
const noParentFolderOption = lang.get("comboBoxSelectionNone_msg")
|
||||
const mailGroupId = mailBoxDetail.mailGroup._id
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailBoxDetail.mailbox.folders)._id)
|
||||
let folderNameValue = editedFolder?.name ?? ""
|
||||
let targetFolders: SelectorItemList<MailFolder | null> = mailBoxDetail.folders
|
||||
let targetFolders: SelectorItemList<MailFolder | null> = folders
|
||||
.getIndentedList(editedFolder)
|
||||
// filter: SPAM and TRASH and descendants are only shown if editing (folders can only be moved there, not created there)
|
||||
.filter((folderInfo) => !(editedFolder === null && isSpamOrTrashFolder(mailBoxDetail.folders, folderInfo.folder)))
|
||||
.map((folderInfo) => {
|
||||
.filter((folderInfo: IndentedFolder) => !(editedFolder === null && isSpamOrTrashFolder(folders, folderInfo.folder)))
|
||||
.map((folderInfo: IndentedFolder) => {
|
||||
return {
|
||||
name: getIndentedFolderNameForDropdown(folderInfo),
|
||||
value: folderInfo.folder,
|
||||
|
@ -49,7 +53,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
|
|||
selectedValue: selectedParentFolder,
|
||||
selectedValueDisplay: selectedParentFolder ? getFolderName(selectedParentFolder) : noParentFolderOption,
|
||||
selectionChangedHandler: (newFolder: MailFolder | null) => (selectedParentFolder = newFolder),
|
||||
helpLabel: () => (selectedParentFolder ? getPathToFolderString(mailBoxDetail.folders, selectedParentFolder) : ""),
|
||||
helpLabel: () => (selectedParentFolder ? getPathToFolderString(folders, selectedParentFolder) : ""),
|
||||
}),
|
||||
]
|
||||
|
||||
|
@ -91,7 +95,7 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
|
|||
if (!confirmed) return
|
||||
|
||||
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
|
||||
await locator.mailModel.trashFolderAndSubfolders(editedFolder)
|
||||
await mailLocator.mailModel.trashFolderAndSubfolders(editedFolder)
|
||||
} else if (selectedParentFolder?.folderType === MailSetKind.SPAM && !isSameId(selectedParentFolder._id, editedFolder.parentFolder)) {
|
||||
// if it is being moved to spam (and not already in spam), ask about reporting containing emails
|
||||
const confirmed = await Dialog.confirm(() =>
|
||||
|
@ -102,16 +106,16 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
|
|||
if (!confirmed) return
|
||||
|
||||
// get mails to report before moving to mail model
|
||||
const descendants = mailBoxDetail.folders.getDescendantFoldersOfParent(editedFolder._id).sort((l, r) => r.level - l.level)
|
||||
const descendants = folders.getDescendantFoldersOfParent(editedFolder._id).sort((l: IndentedFolder, r: IndentedFolder) => r.level - l.level)
|
||||
let reportableMails: Array<Mail> = []
|
||||
await loadAllMailsOfFolder(editedFolder, reportableMails)
|
||||
for (const descendant of descendants) {
|
||||
await loadAllMailsOfFolder(descendant.folder, reportableMails)
|
||||
}
|
||||
await reportMailsAutomatically(MailReportType.SPAM, locator.mailModel, mailBoxDetail, reportableMails)
|
||||
await reportMailsAutomatically(MailReportType.SPAM, locator.mailboxModel, mailLocator.mailModel, mailBoxDetail, reportableMails)
|
||||
|
||||
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
|
||||
await locator.mailModel.sendFolderToSpam(editedFolder)
|
||||
await mailLocator.mailModel.sendFolderToSpam(editedFolder)
|
||||
} else {
|
||||
await locator.mailFacade.updateMailFolderName(editedFolder, folderNameValue)
|
||||
await locator.mailFacade.updateMailFolderParent(editedFolder, selectedParentFolder?._id || null)
|
||||
|
@ -127,16 +131,22 @@ export async function showEditFolderDialog(mailBoxDetail: MailboxDetail, editedF
|
|||
Dialog.showActionDialog({
|
||||
title: editedFolder ? lang.get("editFolder_action") : lang.get("addFolder_action"),
|
||||
child: form,
|
||||
validator: () => checkFolderName(mailBoxDetail, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null),
|
||||
validator: () => checkFolderName(mailBoxDetail, folders, folderNameValue, mailGroupId, selectedParentFolder?._id ?? null),
|
||||
allowOkWithReturn: true,
|
||||
okAction: okAction,
|
||||
})
|
||||
}
|
||||
|
||||
function checkFolderName(mailboxDetail: MailboxDetail, name: string, mailGroupId: Id, parentFolderId: IdTuple | null): TranslationKey | null {
|
||||
function checkFolderName(
|
||||
mailboxDetail: MailboxDetail,
|
||||
folders: FolderSystem,
|
||||
name: string,
|
||||
mailGroupId: Id,
|
||||
parentFolderId: IdTuple | null,
|
||||
): TranslationKey | null {
|
||||
if (name.trim() === "") {
|
||||
return "folderNameNeutral_msg"
|
||||
} else if (mailboxDetail.folders.getCustomFoldersOfParent(parentFolderId).some((f) => f.name === name)) {
|
||||
} else if (folders.getCustomFoldersOfParent(parentFolderId).some((f) => f.name === name)) {
|
||||
return "folderNameInvalidExisting_msg"
|
||||
} else {
|
||||
return null
|
||||
|
|
|
@ -100,7 +100,10 @@ export function sendResponse(event: CalendarEvent, recipient: string, status: Ca
|
|||
return
|
||||
}
|
||||
|
||||
const replyResult = await calendarInviteHandler.replyToEventInvitation(latestEvent, ownAttendee, status, previousMail)
|
||||
const mailboxDetails = await mailLocator.mailModel.getMailboxDetailsForMail(previousMail)
|
||||
if (mailboxDetails == null) return
|
||||
|
||||
const replyResult = await calendarInviteHandler.replyToEventInvitation(latestEvent, ownAttendee, status, previousMail, mailboxDetails)
|
||||
if (replyResult === ReplyResult.ReplySent) {
|
||||
ownAttendee.status = status
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import m, { Child, Children, Component, Vnode } from "mithril"
|
||||
import { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { locator } from "../../../common/api/main/CommonLocator.js"
|
||||
import { SidebarSection } from "../../../common/gui/SidebarSection.js"
|
||||
import { IconButton, IconButtonAttrs } from "../../../common/gui/base/IconButton.js"
|
||||
import { FolderSubtree } from "../../../common/api/common/mail/FolderSystem.js"
|
||||
import { FolderSubtree, FolderSystem } from "../../../common/api/common/mail/FolderSystem.js"
|
||||
import { elementIdPart, getElementId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { isSelectedPrefix, NavButtonAttrs, NavButtonColor } from "../../../common/gui/base/NavButton.js"
|
||||
import { MAIL_PREFIX } from "../../../common/misc/RouteChange.js"
|
||||
import { MailFolderRow } from "./MailFolderRow.js"
|
||||
import { last, noOp, Thunk } from "@tutao/tutanota-utils"
|
||||
import { assertNotNull, last, noOp, Thunk } from "@tutao/tutanota-utils"
|
||||
import { MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { attachDropdown, DropdownButtonAttrs } from "../../../common/gui/base/Dropdown.js"
|
||||
import { Icons } from "../../../common/gui/base/icons/Icons.js"
|
||||
|
@ -18,9 +18,11 @@ import { MailSetKind } from "../../../common/api/common/TutanotaConstants.js"
|
|||
import { px, size } from "../../../common/gui/size.js"
|
||||
import { RowButton } from "../../../common/gui/base/buttons/RowButton.js"
|
||||
import { getFolderIcon, getFolderName, MAX_FOLDER_INDENT_LEVEL } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { isSpamOrTrashFolder, MailModel } from "../model/MailModel.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
export interface MailFolderViewAttrs {
|
||||
mailModel: MailModel
|
||||
mailboxDetail: MailboxDetail
|
||||
mailFolderElementIdToSelectedMailId: ReadonlyMap<Id, Id>
|
||||
onFolderClick: (folder: MailFolder) => unknown
|
||||
|
@ -41,20 +43,21 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
private visibleRow: string | null = null
|
||||
|
||||
view({ attrs }: Vnode<MailFolderViewAttrs>): Children {
|
||||
const { mailboxDetail } = attrs
|
||||
const groupCounters = locator.mailModel.mailboxCounters()[mailboxDetail.mailGroup._id] || {}
|
||||
const { mailboxDetail, mailModel } = attrs
|
||||
const groupCounters = mailModel.mailboxCounters()[mailboxDetail.mailGroup._id] || {}
|
||||
const folders = mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
// Important: this array is keyed so each item must have a key and `null` cannot be in the array
|
||||
// So instead we push or not push into array
|
||||
const customSystems = mailboxDetail.folders.customSubtrees
|
||||
const systemSystems = mailboxDetail.folders.systemSubtrees
|
||||
const customSystems = folders.customSubtrees
|
||||
const systemSystems = folders.systemSubtrees
|
||||
const children: Children = []
|
||||
const selectedFolder = mailboxDetail.folders
|
||||
const selectedFolder = folders
|
||||
.getIndentedList()
|
||||
.map((f) => f.folder)
|
||||
.find((f) => isSelectedPrefix(MAIL_PREFIX + "/" + getElementId(f)))
|
||||
const path = selectedFolder ? mailboxDetail.folders.getPathToFolder(selectedFolder._id) : []
|
||||
const path = selectedFolder ? folders.getPathToFolder(selectedFolder._id) : []
|
||||
const isInternalUser = locator.logins.isInternalUserLoggedIn()
|
||||
const systemChildren = this.renderFolderTree(systemSystems, groupCounters, attrs, path, isInternalUser)
|
||||
const systemChildren = this.renderFolderTree(systemSystems, groupCounters, folders, attrs, path, isInternalUser)
|
||||
if (systemChildren) {
|
||||
children.push(...systemChildren.children)
|
||||
}
|
||||
|
@ -67,7 +70,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
button: attrs.inEditMode ? this.renderCreateFolderAddButton(null, attrs) : this.renderEditFoldersButton(attrs),
|
||||
key: "yourFolders", // we need to set a key because folder rows also have a key.
|
||||
},
|
||||
this.renderFolderTree(customSystems, groupCounters, attrs, path, isInternalUser).children,
|
||||
this.renderFolderTree(customSystems, groupCounters, folders, attrs, path, isInternalUser).children,
|
||||
),
|
||||
)
|
||||
children.push(this.renderAddFolderButtonRow(attrs))
|
||||
|
@ -78,6 +81,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
private renderFolderTree(
|
||||
subSystems: readonly FolderSubtree[],
|
||||
groupCounters: Counters,
|
||||
folders: FolderSystem,
|
||||
attrs: MailFolderViewAttrs,
|
||||
path: MailFolder[],
|
||||
isInternalUser: boolean,
|
||||
|
@ -115,13 +119,13 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
const summedCount = !currentExpansionState && hasChildren ? this.getTotalFolderCounter(groupCounters, system) : groupCounters[counterId]
|
||||
const childResult =
|
||||
hasChildren && currentExpansionState
|
||||
? this.renderFolderTree(system.children, groupCounters, attrs, path, isInternalUser, indentationLevel + 1)
|
||||
? this.renderFolderTree(system.children, groupCounters, folders, attrs, path, isInternalUser, indentationLevel + 1)
|
||||
: { children: null, numRows: 0 }
|
||||
const isTrashOrSpam = system.folder.folderType === MailSetKind.TRASH || system.folder.folderType === MailSetKind.SPAM
|
||||
const isRightButtonVisible = this.visibleRow === id
|
||||
const rightButton =
|
||||
isInternalUser && !isTrashOrSpam && (isRightButtonVisible || attrs.inEditMode)
|
||||
? this.createFolderMoreButton(system.folder, attrs, () => {
|
||||
? this.createFolderMoreButton(system.folder, folders, attrs, () => {
|
||||
this.visibleRow = null
|
||||
})
|
||||
: null
|
||||
|
@ -177,7 +181,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
return (counters[counterId] ?? 0) + system.children.reduce((acc, child) => acc + this.getTotalFolderCounter(counters, child), 0)
|
||||
}
|
||||
|
||||
private createFolderMoreButton(folder: MailFolder, attrs: MailFolderViewAttrs, onClose: Thunk): IconButtonAttrs {
|
||||
private createFolderMoreButton(folder: MailFolder, folders: FolderSystem, attrs: MailFolderViewAttrs, onClose: Thunk): IconButtonAttrs {
|
||||
return attachDropdown({
|
||||
mainButtonAttrs: {
|
||||
title: "more_label",
|
||||
|
@ -188,9 +192,9 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
childAttrs: () => {
|
||||
return folder.folderType === MailSetKind.CUSTOM
|
||||
? // cannot add new folder to custom folder in spam or trash folder
|
||||
isSpamOrTrashFolder(attrs.mailboxDetail.folders, folder)
|
||||
? [this.editButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
|
||||
: [this.editButtonAttrs(attrs, folder), this.addButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
|
||||
isSpamOrTrashFolder(folders, folder)
|
||||
? [this.editButtonAttrs(attrs, folders, folder), this.deleteButtonAttrs(attrs, folder)]
|
||||
: [this.editButtonAttrs(attrs, folders, folder), this.addButtonAttrs(attrs, folder), this.deleteButtonAttrs(attrs, folder)]
|
||||
: [this.addButtonAttrs(attrs, folder)]
|
||||
},
|
||||
onClose,
|
||||
|
@ -217,7 +221,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
}
|
||||
}
|
||||
|
||||
private editButtonAttrs(attrs: MailFolderViewAttrs, folder: MailFolder): DropdownButtonAttrs {
|
||||
private editButtonAttrs(attrs: MailFolderViewAttrs, folders: FolderSystem, folder: MailFolder): DropdownButtonAttrs {
|
||||
return {
|
||||
label: "edit_action",
|
||||
icon: Icons.Edit,
|
||||
|
@ -225,7 +229,7 @@ export class MailFoldersView implements Component<MailFolderViewAttrs> {
|
|||
attrs.onShowFolderAddEditDialog(
|
||||
attrs.mailboxDetail.mailGroup._id,
|
||||
folder,
|
||||
folder.parentFolder ? attrs.mailboxDetail.folders.getFolderById(elementIdPart(folder.parentFolder)) : null,
|
||||
folder.parentFolder ? folders.getFolderById(elementIdPart(folder.parentFolder)) : null,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import type { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { createMail } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import type { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { createMail, File as TutanotaFile, Mail, MailFolder } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { LockedError, PreconditionFailedError } from "../../../common/api/common/error/RestError"
|
||||
import { Dialog } from "../../../common/gui/base/Dialog"
|
||||
import { locator } from "../../../common/api/main/CommonLocator"
|
||||
import { AllIcons } from "../../../common/gui/base/Icon"
|
||||
import { Icons } from "../../../common/gui/base/icons/Icons"
|
||||
import { isApp, isDesktop } from "../../../common/api/common/Env"
|
||||
import { assertNotNull, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
|
||||
import { MailReportType, MailSetKind } from "../../../common/api/common/TutanotaConstants"
|
||||
import { assertNotNull, endsWith, neverNull, noOp, promiseMap } from "@tutao/tutanota-utils"
|
||||
import { MailReportType, MailSetKind, MailState, SYSTEM_GROUP_MAIL_ADDRESS } from "../../../common/api/common/TutanotaConstants"
|
||||
import { getElementId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { reportMailsAutomatically } from "./MailReportDialog"
|
||||
import { DataFile } from "../../../common/api/common/DataFile"
|
||||
import { lang, TranslationKey } from "../../../common/misc/LanguageViewModel"
|
||||
|
@ -20,26 +20,25 @@ import { size } from "../../../common/gui/size.js"
|
|||
import { PinchZoom } from "../../../common/gui/PinchZoom.js"
|
||||
import { InlineImageReference, InlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
|
||||
import {
|
||||
assertSystemFolderOfType,
|
||||
getFolderIcon,
|
||||
getFolderName,
|
||||
getIndentedFolderNameForDropdown,
|
||||
getMoveTargetFolderSystems,
|
||||
isOfTypeOrSubfolderOf,
|
||||
hasValidEncryptionAuthForTeamOrSystemMail,
|
||||
} from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { getElementId } from "../../../common/api/common/utils/EntityUtils.js"
|
||||
import { assertSystemFolderOfType, getMoveTargetFolderSystems, isOfTypeOrSubfolderOf, isSpamOrTrashFolder, MailModel } from "../model/MailModel.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
import type { FolderSystem } from "../../../common/api/common/mail/FolderSystem.js"
|
||||
|
||||
export async function showDeleteConfirmationDialog(mails: ReadonlyArray<Mail>): Promise<boolean> {
|
||||
let trashMails: Mail[] = []
|
||||
let moveMails: Mail[] = []
|
||||
for (let mail of mails) {
|
||||
const folder = locator.mailModel.getMailFolderForMail(mail)
|
||||
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMail(mail)
|
||||
if (mailboxDetail == null) {
|
||||
const folder = mailLocator.mailModel.getMailFolderForMail(mail)
|
||||
const folders = await mailLocator.mailModel.getMailboxFoldersForMail(mail)
|
||||
if (folders == null) {
|
||||
continue
|
||||
}
|
||||
const isFinalDelete = folder && isSpamOrTrashFolder(mailboxDetail.folders, folder)
|
||||
const isFinalDelete = folder && isSpamOrTrashFolder(folders, folder)
|
||||
isFinalDelete ? trashMails.push(mail) : moveMails.push(mail)
|
||||
}
|
||||
|
||||
|
@ -85,6 +84,7 @@ export function promptAndDeleteMails(mailModel: MailModel, mails: ReadonlyArray<
|
|||
}
|
||||
|
||||
interface MoveMailsParams {
|
||||
mailboxModel: MailboxModel
|
||||
mailModel: MailModel
|
||||
mails: ReadonlyArray<Mail>
|
||||
targetMailFolder: MailFolder
|
||||
|
@ -95,12 +95,12 @@ interface MoveMailsParams {
|
|||
* Moves the mails and reports them as spam if the user or settings allow it.
|
||||
* @return whether mails were actually moved
|
||||
*/
|
||||
export async function moveMails({ mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
|
||||
export async function moveMails({ mailboxModel, mailModel, mails, targetMailFolder, isReportable = true }: MoveMailsParams): Promise<boolean> {
|
||||
const details = await mailModel.getMailboxDetailsForMailFolder(targetMailFolder)
|
||||
if (details == null) {
|
||||
if (details == null || details.mailbox.folders == null) {
|
||||
return false
|
||||
}
|
||||
const system = details.folders
|
||||
const system = mailModel.getMailboxFoldersForId(details.mailbox.folders._id)
|
||||
return mailModel
|
||||
.moveMails(mails, targetMailFolder)
|
||||
.then(async () => {
|
||||
|
@ -111,8 +111,8 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
|
|||
reportableMail._id = targetMailFolder.isMailSet ? mail._id : [targetMailFolder.mails, getElementId(mail)]
|
||||
return reportableMail
|
||||
})
|
||||
const mailboxDetails = await mailModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
|
||||
await reportMailsAutomatically(MailReportType.SPAM, mailModel, mailboxDetails, reportableMails)
|
||||
const mailboxDetails = await mailboxModel.getMailboxDetailsForMailGroup(assertNotNull(targetMailFolder._ownerGroup))
|
||||
await reportMailsAutomatically(MailReportType.SPAM, mailboxModel, mailModel, mailboxDetails, reportableMails)
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -130,10 +130,11 @@ export async function moveMails({ mailModel, mails, targetMailFolder, isReportab
|
|||
export function archiveMails(mails: Mail[]): Promise<void> {
|
||||
if (mails.length > 0) {
|
||||
// assume all mails in the array belong to the same Mailbox
|
||||
return locator.mailModel.getMailboxFolders(mails[0]).then((folders) => {
|
||||
return mailLocator.mailModel.getMailboxFoldersForMail(mails[0]).then((folders: FolderSystem) => {
|
||||
folders &&
|
||||
moveMails({
|
||||
mailModel: locator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mails: mails,
|
||||
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.ARCHIVE),
|
||||
})
|
||||
|
@ -146,10 +147,11 @@ export function archiveMails(mails: Mail[]): Promise<void> {
|
|||
export function moveToInbox(mails: Mail[]): Promise<any> {
|
||||
if (mails.length > 0) {
|
||||
// assume all mails in the array belong to the same Mailbox
|
||||
return locator.mailModel.getMailboxFolders(mails[0]).then((folders) => {
|
||||
return mailLocator.mailModel.getMailboxFoldersForMail(mails[0]).then((folders: FolderSystem) => {
|
||||
folders &&
|
||||
moveMails({
|
||||
mailModel: locator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mails: mails,
|
||||
targetMailFolder: assertSystemFolderOfType(folders, MailSetKind.INBOX),
|
||||
})
|
||||
|
@ -160,7 +162,7 @@ export function moveToInbox(mails: Mail[]): Promise<any> {
|
|||
}
|
||||
|
||||
export function getMailFolderIcon(mail: Mail): AllIcons {
|
||||
let folder = locator.mailModel.getMailFolderForMail(mail)
|
||||
let folder = mailLocator.mailModel.getMailFolderForMail(mail)
|
||||
|
||||
if (folder) {
|
||||
return getFolderIcon(folder)
|
||||
|
@ -291,6 +293,7 @@ export function getReferencedAttachments(attachments: Array<TutanotaFile>, refer
|
|||
}
|
||||
|
||||
export async function showMoveMailsDropdown(
|
||||
mailboxModel: MailboxModel,
|
||||
model: MailModel,
|
||||
origin: PosRect,
|
||||
mails: readonly Mail[],
|
||||
|
@ -307,7 +310,7 @@ export async function showMoveMailsDropdown(
|
|||
text: () => getIndentedFolderNameForDropdown(f),
|
||||
click: () => {
|
||||
onSelected()
|
||||
moveMails({ mailModel: model, mails: mails, targetMailFolder: f.folder })
|
||||
moveMails({ mailboxModel, mailModel: model, mails: mails, targetMailFolder: f.folder })
|
||||
},
|
||||
icon: getFolderIcon(f.folder),
|
||||
} satisfies DropdownChildAttrs),
|
||||
|
@ -335,3 +338,24 @@ export function getMoveMailBounds(): PosRect {
|
|||
// just putting the move mail dropdown in the left side of the viewport with a bit of margin
|
||||
return new DomRectReadOnlyPolyfilled(size.hpad_large, size.vpad_large, 0, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: DOES NOT VERIFY IF THE MESSAGE IS AUTHENTIC - DO NOT USE THIS OUTSIDE OF THIS FILE OR FOR TESTING
|
||||
* @VisibleForTesting
|
||||
*/
|
||||
export function isTutanotaTeamAddress(address: string): boolean {
|
||||
return endsWith(address, "@tutao.de") || address === "no-reply@tutanota.de"
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a tutao team member email or a system notification
|
||||
*/
|
||||
export function isTutanotaTeamMail(mail: Mail): boolean {
|
||||
const { confidential, sender, state } = mail
|
||||
return (
|
||||
confidential &&
|
||||
state === MailState.RECEIVED &&
|
||||
hasValidEncryptionAuthForTeamOrSystemMail(mail) &&
|
||||
(sender.address === SYSTEM_GROUP_MAIL_ADDRESS || isTutanotaTeamAddress(sender.address))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -29,8 +29,10 @@ import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js"
|
|||
import { theme } from "../../../common/gui/theme.js"
|
||||
import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
|
||||
import { isKeyPressed } from "../../../common/misc/KeyManager.js"
|
||||
import { assertSystemFolderOfType, canDoDragAndDropExport, isOfTypeOrSubfolderOf } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { ListModel } from "../../../common/misc/ListModel.js"
|
||||
import { canDoDragAndDropExport } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { assertSystemFolderOfType, isOfTypeOrSubfolderOf } from "../model/MailModel.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -402,14 +404,16 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
const selectedFolder = this.mailViewModel.getFolder()
|
||||
if (selectedFolder) {
|
||||
const mailDetails = await this.mailViewModel.getMailboxDetails()
|
||||
return isOfTypeOrSubfolderOf(mailDetails.folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH
|
||||
} else {
|
||||
return false
|
||||
if (mailDetails.mailbox.folders) {
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailDetails.mailbox.folders._id)
|
||||
return isOfTypeOrSubfolderOf(folders, selectedFolder, MailSetKind.ARCHIVE) || selectedFolder.folderType === MailSetKind.TRASH
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private async onSwipeLeft(listElement: Mail): Promise<ListSwipeDecision> {
|
||||
const wereDeleted = await promptAndDeleteMails(locator.mailModel, [listElement], () => this.mailViewModel.listModel?.selectNone())
|
||||
const wereDeleted = await promptAndDeleteMails(mailLocator.mailModel, [listElement], () => this.mailViewModel.listModel?.selectNone())
|
||||
return wereDeleted ? ListSwipeDecision.Commit : ListSwipeDecision.Cancel
|
||||
}
|
||||
|
||||
|
@ -419,7 +423,7 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
this.mailViewModel.listModel?.selectNone()
|
||||
return ListSwipeDecision.Cancel
|
||||
} else {
|
||||
const folders = await locator.mailModel.getMailboxFolders(listElement)
|
||||
const folders = await mailLocator.mailModel.getMailboxFoldersForMail(listElement)
|
||||
if (folders) {
|
||||
//Check if the user is in the trash/spam folder or if it's in Inbox or Archive
|
||||
//to determinate the target folder
|
||||
|
@ -427,7 +431,8 @@ export class MailListView implements Component<MailListViewAttrs> {
|
|||
? this.getRecoverFolder(listElement, folders)
|
||||
: assertNotNull(folders.getSystemFolderByType(this.showingArchive ? MailSetKind.INBOX : MailSetKind.ARCHIVE))
|
||||
const wereMoved = await moveMails({
|
||||
mailModel: locator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mails: [listElement],
|
||||
targetMailFolder,
|
||||
})
|
||||
|
|
|
@ -5,8 +5,9 @@ import m from "mithril"
|
|||
import { MailReportType, ReportMovedMailsType } from "../../../common/api/common/TutanotaConstants"
|
||||
import { ButtonAttrs, ButtonType } from "../../../common/gui/base/Button.js"
|
||||
import { Dialog } from "../../../common/gui/base/Dialog"
|
||||
import type { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { showSnackBar } from "../../../common/gui/base/SnackBar"
|
||||
import { MailModel } from "../model/MailModel.js"
|
||||
|
||||
function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDetail): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
|
@ -60,6 +61,7 @@ function confirmMailReportDialog(mailModel: MailModel, mailboxDetails: MailboxDe
|
|||
*/
|
||||
export async function reportMailsAutomatically(
|
||||
mailReportType: MailReportType,
|
||||
mailboxModel: MailboxModel,
|
||||
mailModel: MailModel,
|
||||
mailboxDetails: MailboxDetail,
|
||||
mails: ReadonlyArray<Mail>,
|
||||
|
@ -68,7 +70,7 @@ export async function reportMailsAutomatically(
|
|||
return
|
||||
}
|
||||
|
||||
const mailboxProperties = await mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
let allowUndoing = true // decides if a snackbar is shown to prevent the server request
|
||||
|
||||
let isReportable = false
|
||||
|
|
|
@ -20,7 +20,9 @@ import { px, size } from "../../../common/gui/size.js"
|
|||
import { NBSP, noOp } from "@tutao/tutanota-utils"
|
||||
import { VirtualRow } from "../../../common/gui/base/ListUtils.js"
|
||||
import { companyTeamLabel } from "../../../common/misc/ClientConstants.js"
|
||||
import { getConfidentialFontIcon, getSenderOrRecipientHeading, isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getConfidentialFontIcon, getSenderOrRecipientHeading } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isTutanotaTeamMail } from "./MailGuiUtils.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
const iconMap: Record<MailSetKind, string> = {
|
||||
[MailSetKind.CUSTOM]: FontIcons.Folder,
|
||||
|
@ -255,7 +257,7 @@ export class MailRow implements VirtualRow<Mail> {
|
|||
let iconText = ""
|
||||
|
||||
if (this.showFolderIcon) {
|
||||
let folder = locator.mailModel.getMailFolderForMail(mail)
|
||||
let folder = mailLocator.mailModel.getMailFolderForMail(mail)
|
||||
iconText += folder ? this.folderIcon(getMailFolderType(folder)) : ""
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { keyManager } from "../../../common/misc/KeyManager"
|
|||
import { getMailSelectionMessage, MultiItemViewer } from "./MultiItemViewer.js"
|
||||
import { Icons } from "../../../common/gui/base/icons/Icons"
|
||||
import { showProgressDialog } from "../../../common/gui/dialogs/ProgressDialog"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { locator } from "../../../common/api/main/CommonLocator"
|
||||
import { PermissionError } from "../../../common/api/common/error/PermissionError"
|
||||
import { styles } from "../../../common/gui/styles"
|
||||
|
@ -58,8 +58,9 @@ import { MailFilterButton } from "./MailFilterButton.js"
|
|||
import { listSelectionKeyboardShortcuts } from "../../../common/gui/base/ListUtils.js"
|
||||
import { canDoDragAndDropExport, getFolderName, getMailboxName } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { BottomNav } from "../../gui/BottomNav.js"
|
||||
import { isSpamOrTrashFolder } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { showSnackBar } from "../../../common/gui/base/SnackBar.js"
|
||||
import { isSpamOrTrashFolder } from "../model/MailModel.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -227,7 +228,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
vnode.attrs.mailViewModel.init()
|
||||
|
||||
this.oncreate = () => {
|
||||
this.countersStream = locator.mailModel.mailboxCounters.map(m.redraw)
|
||||
this.countersStream = mailLocator.mailModel.mailboxCounters.map(m.redraw)
|
||||
keyManager.registerShortcuts(shortcuts)
|
||||
this.cache.conversationViewPreference = deviceConfig.getConversationViewShowOnlySelectedMail()
|
||||
}
|
||||
|
@ -249,6 +250,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
|
||||
private mailViewerSingleActions(viewModel: ConversationViewModel) {
|
||||
return m(MailViewerActions, {
|
||||
mailboxModel: viewModel.primaryViewModel().mailboxModel,
|
||||
mailModel: viewModel.primaryViewModel().mailModel,
|
||||
mailViewerViewModel: viewModel.primaryViewModel(),
|
||||
mails: [viewModel.primaryMail],
|
||||
|
@ -283,7 +285,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
|
||||
private mailViewerMultiActions() {
|
||||
return m(MailViewerActions, {
|
||||
mailModel: locator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mails: this.mailViewModel.listModel?.getSelectedAsArray() ?? [],
|
||||
selectNone: () => this.mailViewModel.listModel?.selectNone(),
|
||||
})
|
||||
|
@ -375,7 +378,8 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
? m(MobileMailMultiselectionActionBar, {
|
||||
mails: this.mailViewModel.listModel.getSelectedAsArray(),
|
||||
selectNone: () => this.mailViewModel.listModel?.selectNone(),
|
||||
mailModel: locator.mailModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
})
|
||||
: m(BottomNav),
|
||||
}),
|
||||
|
@ -545,7 +549,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
|
||||
const selectedMails = mailList.getSelectedAsArray()
|
||||
|
||||
showMoveMailsDropdown(locator.mailModel, getMoveMailBounds(), selectedMails)
|
||||
showMoveMailsDropdown(locator.mailboxModel, mailLocator.mailModel, getMoveMailBounds(), selectedMails)
|
||||
}
|
||||
|
||||
private createFolderColumn(editingFolderForMailGroup: Id | null = null, drawerAttrs: DrawerMenuAttrs) {
|
||||
|
@ -577,7 +581,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
}
|
||||
|
||||
private renderFolders(editingFolderForMailGroup: Id | null) {
|
||||
const details = locator.mailModel.mailboxDetails() ?? []
|
||||
const details = locator.mailboxModel.mailboxDetails() ?? []
|
||||
return [
|
||||
...details.map((mailboxDetail) => {
|
||||
const inEditMode = editingFolderForMailGroup === mailboxDetail.mailGroup._id
|
||||
|
@ -601,6 +605,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
|
||||
private createMailboxFolderItems(mailboxDetail: MailboxDetail, inEditMode: boolean, onEditMailbox: () => void): Children {
|
||||
return m(MailFoldersView, {
|
||||
mailModel: mailLocator.mailModel,
|
||||
mailboxDetail,
|
||||
expandedFolders: this.expandedState,
|
||||
mailFolderElementIdToSelectedMailId: this.mailViewModel.getMailFolderToSelectedMail(),
|
||||
|
@ -628,7 +633,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
if (location.hash.length > 5) {
|
||||
let url = location.hash.substring(5)
|
||||
let decodedUrl = decodeURIComponent(url)
|
||||
Promise.all([locator.mailModel.getUserMailboxDetails(), import("../editor/MailEditor")]).then(
|
||||
Promise.all([locator.mailboxModel.getUserMailboxDetails(), import("../editor/MailEditor")]).then(
|
||||
([mailboxDetails, { newMailtoUrlMailEditor }]) => {
|
||||
newMailtoUrlMailEditor(decodedUrl, false, mailboxDetails)
|
||||
.then((editor) => editor.show())
|
||||
|
@ -693,7 +698,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
}
|
||||
}
|
||||
|
||||
moveMails({ mailModel: locator.mailModel, mails: mailsToMove, targetMailFolder: folder })
|
||||
moveMails({ mailboxModel: locator.mailboxModel, mailModel: mailLocator.mailModel, mails: mailsToMove, targetMailFolder: folder })
|
||||
}
|
||||
|
||||
private async showNewMailDialog(): Promise<void> {
|
||||
|
@ -713,15 +718,19 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
|
||||
// remove any selection to avoid that the next mail is loaded and selected for each deleted mail event
|
||||
this.mailViewModel?.listModel?.selectNone()
|
||||
if (mailboxDetail.mailbox.folders == null) {
|
||||
return
|
||||
}
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
|
||||
if (isSpamOrTrashFolder(mailboxDetail.folders, folder)) {
|
||||
if (isSpamOrTrashFolder(folders, folder)) {
|
||||
const confirmed = await Dialog.confirm(() =>
|
||||
lang.get("confirmDeleteFinallyCustomFolder_msg", {
|
||||
"{1}": getFolderName(folder),
|
||||
}),
|
||||
)
|
||||
if (!confirmed) return
|
||||
await locator.mailModel.finallyDeleteCustomMailFolder(folder)
|
||||
await mailLocator.mailModel.finallyDeleteCustomMailFolder(folder)
|
||||
} else {
|
||||
const confirmed = await Dialog.confirm(() =>
|
||||
lang.get("confirmDeleteCustomFolder_msg", {
|
||||
|
@ -729,7 +738,7 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
}),
|
||||
)
|
||||
if (!confirmed) return
|
||||
await locator.mailModel.trashFolderAndSubfolders(folder)
|
||||
await mailLocator.mailModel.trashFolderAndSubfolders(folder)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -742,15 +751,15 @@ export class MailView extends BaseTopLevelView implements TopLevelView<MailViewA
|
|||
return
|
||||
}
|
||||
// set all selected emails to the opposite of the first email's unread state
|
||||
await locator.mailModel.markMails(mails, !mails[0].unread)
|
||||
await mailLocator.mailModel.markMails(mails, !mails[0].unread)
|
||||
}
|
||||
|
||||
private deleteMails(mails: Mail[]): Promise<boolean> {
|
||||
return promptAndDeleteMails(locator.mailModel, mails, noOp)
|
||||
return promptAndDeleteMails(mailLocator.mailModel, mails, noOp)
|
||||
}
|
||||
|
||||
private async showFolderAddEditDialog(mailGroupId: Id, folder: MailFolder | null, parentFolder: MailFolder | null) {
|
||||
const mailboxDetail = await locator.mailModel.getMailboxDetailsForMailGroup(mailGroupId)
|
||||
const mailboxDetail = await locator.mailboxModel.getMailboxDetailsForMailGroup(mailGroupId)
|
||||
await showEditFolderDialog(mailboxDetail, folder, parentFolder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ListModel } from "../../../common/misc/ListModel.js"
|
||||
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { Mail, MailFolder, MailSetEntry, MailSetEntryTypeRef, MailTypeRef } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import {
|
||||
|
@ -45,8 +45,8 @@ import { Router } from "../../../common/gui/ScopedRouter.js"
|
|||
import { ListFetchResult } from "../../../common/gui/base/ListUtils.js"
|
||||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { EventController } from "../../../common/api/main/EventController.js"
|
||||
import { assertSystemFolderOfType, getMailFilterForType, isOfTypeOrSubfolderOf, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSpamOrTrashFolder, isSubfolderOfType } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { assertSystemFolderOfType, isOfTypeOrSubfolderOf, isSpamOrTrashFolder, isSubfolderOfType, MailModel } from "../model/MailModel.js"
|
||||
import { CacheMode } from "../../../common/api/worker/rest/EntityRestClient.js"
|
||||
|
||||
export interface MailOpenedListener {
|
||||
|
@ -96,6 +96,7 @@ export class MailViewModel {
|
|||
private conversationPref: boolean = false
|
||||
|
||||
constructor(
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly entityClient: EntityClient,
|
||||
private readonly eventController: EventController,
|
||||
|
@ -126,13 +127,15 @@ export class MailViewModel {
|
|||
|
||||
async showMailWithFolderId(folderId?: Id, mailId?: Id): Promise<void> {
|
||||
if (folderId) {
|
||||
const mailboxDetails = await this.mailModel.getMailboxDetails()
|
||||
const mailboxDetail: MailboxDetail | null = mailboxDetails.find((md) => md.folders.getFolderById(folderId)) ?? null
|
||||
const folder = mailboxDetail?.folders.getFolderById(folderId)
|
||||
return this.showMail(folder, mailId)
|
||||
} else {
|
||||
return this.showMail(null, mailId)
|
||||
const folderStructures = this.mailModel.folders()
|
||||
for (const folders of Object.values(folderStructures)) {
|
||||
const folder = folders.getFolderById(folderId)
|
||||
if (folder) {
|
||||
return this.showMail(folder, mailId)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.showMail(null, mailId)
|
||||
}
|
||||
|
||||
async showStickyMail(fullMailId: IdTuple, onMissingExplicitMailTarget: () => unknown): Promise<void> {
|
||||
|
@ -288,8 +291,9 @@ export class MailViewModel {
|
|||
}
|
||||
|
||||
private async getFolderForUserInbox(): Promise<MailFolder> {
|
||||
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
|
||||
return assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.INBOX)
|
||||
const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
|
||||
const folders = this.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
return assertSystemFolderOfType(folders, MailSetKind.INBOX)
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -647,7 +651,11 @@ export class MailViewModel {
|
|||
|
||||
async switchToFolder(folderType: Omit<MailSetKind, MailSetKind.CUSTOM>): Promise<void> {
|
||||
const mailboxDetail = assertNotNull(await this.getMailboxDetails())
|
||||
const folder = assertSystemFolderOfType(mailboxDetail.folders, folderType)
|
||||
if (mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
|
||||
return
|
||||
}
|
||||
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
const folder = assertSystemFolderOfType(folders, folderType)
|
||||
await this.showMail(folder, this.mailFolderElementIdToSelectedMailId.get(getElementId(folder)))
|
||||
}
|
||||
|
||||
|
@ -660,8 +668,9 @@ export class MailViewModel {
|
|||
if (!this._folder) return false
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(this._folder)
|
||||
const selectedFolder = this.getFolder()
|
||||
if (selectedFolder && mailboxDetail) {
|
||||
return isOfTypeOrSubfolderOf(mailboxDetail.folders, selectedFolder, MailSetKind.DRAFT)
|
||||
if (selectedFolder && mailboxDetail && mailboxDetail.mailbox.folders) {
|
||||
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
return isOfTypeOrSubfolderOf(folders, selectedFolder, MailSetKind.DRAFT)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
@ -669,16 +678,19 @@ export class MailViewModel {
|
|||
|
||||
async showingTrashOrSpamFolder(): Promise<boolean> {
|
||||
const folder = this.getFolder()
|
||||
if (!folder) {
|
||||
return false
|
||||
if (folder) {
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||
if (folder && mailboxDetail && mailboxDetail.mailbox.folders) {
|
||||
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
return isSpamOrTrashFolder(folders, folder)
|
||||
}
|
||||
}
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMailFolder(folder)
|
||||
return mailboxDetail != null && isSpamOrTrashFolder(mailboxDetail.folders, folder)
|
||||
return false
|
||||
}
|
||||
|
||||
private async mailboxDetailForListWithFallback(folder?: MailFolder | null) {
|
||||
const mailboxDetailForListId = folder ? await this.mailModel.getMailboxDetailsForMailFolder(folder) : null
|
||||
return mailboxDetailForListId ?? (await this.mailModel.getUserMailboxDetails())
|
||||
return mailboxDetailForListId ?? (await this.mailboxModel.getUserMailboxDetails())
|
||||
}
|
||||
|
||||
async finallyDeleteAllMailsInSelectedFolder(folder: MailFolder): Promise<void> {
|
||||
|
@ -694,14 +706,17 @@ export class MailViewModel {
|
|||
throw new UserError("operationStillActive_msg")
|
||||
}),
|
||||
)
|
||||
} else if (isSubfolderOfType(mailboxDetail.folders, folder, MailSetKind.TRASH) || isSubfolderOfType(mailboxDetail.folders, folder, MailSetKind.SPAM)) {
|
||||
return this.mailModel.finallyDeleteCustomMailFolder(folder).catch(
|
||||
ofClass(PreconditionFailedError, () => {
|
||||
throw new UserError("operationStillActive_msg")
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
throw new ProgrammingError(`Cannot delete mails in folder ${String(folder._id)} with type ${folder.folderType}`)
|
||||
const folders = this.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
if (isSubfolderOfType(folders, folder, MailSetKind.TRASH) || isSubfolderOfType(folders, folder, MailSetKind.SPAM)) {
|
||||
return this.mailModel.finallyDeleteCustomMailFolder(folder).catch(
|
||||
ofClass(PreconditionFailedError, () => {
|
||||
throw new UserError("operationStillActive_msg")
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
throw new ProgrammingError(`Cannot delete mails in folder ${String(folder._id)} with type ${folder.folderType}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { theme } from "../../../common/gui/theme"
|
|||
import { client } from "../../../common/misc/ClientDetector"
|
||||
import { styles } from "../../../common/gui/styles"
|
||||
import { DropdownButtonAttrs, showDropdownAtPosition } from "../../../common/gui/base/Dropdown.js"
|
||||
import { replaceCidsWithInlineImages } from "./MailGuiUtils"
|
||||
import { isTutanotaTeamMail, replaceCidsWithInlineImages } from "./MailGuiUtils"
|
||||
import { getCoordsOfMouseOrTouchEvent } from "../../../common/gui/base/GuiUtils"
|
||||
import { copyToClipboard } from "../../../common/misc/ClipboardUtils"
|
||||
import { ContentBlockingStatus, MailViewerViewModel } from "./MailViewerViewModel"
|
||||
|
@ -32,7 +32,7 @@ import { locator } from "../../../common/api/main/CommonLocator.js"
|
|||
import { PinchZoom } from "../../../common/gui/PinchZoom.js"
|
||||
import { responsiveCardHMargin, responsiveCardHPadding } from "../../../common/gui/cards.js"
|
||||
import { Dialog } from "../../../common/gui/base/Dialog.js"
|
||||
import { createNewContact, getExistingRuleForType, isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { createNewContact, getExistingRuleForType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ContentBlockingStatus, MailViewerViewModel } from "./MailViewerViewMode
|
|||
import { canSeeTutaLinks } from "../../../common/gui/base/GuiUtils.js"
|
||||
import { isNotNull, noOp, resolveMaybeLazy } from "@tutao/tutanota-utils"
|
||||
import { IconButton } from "../../../common/gui/base/IconButton.js"
|
||||
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
|
||||
import { isTutanotaTeamMail, promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
|
||||
import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js"
|
||||
import { editDraft, mailViewerMoreActions } from "./MailViewerUtils.js"
|
||||
import { liveDataAttrs } from "../../../common/gui/AriaUtils.js"
|
||||
|
@ -28,7 +28,6 @@ import { AttachmentBubble, getAttachmentType } from "../../../common/gui/Attachm
|
|||
import { responsiveCardHMargin, responsiveCardHPadding } from "../../../common/gui/cards.js"
|
||||
import { companyTeamLabel } from "../../../common/misc/ClientConstants.js"
|
||||
import { getConfidentialIcon, getFolderIconByType, getMailAddressDisplayText } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isTutanotaTeamMail } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { MailAddressAndName } from "../../../common/api/common/CommonMailUtils.js"
|
||||
|
||||
export type MailAddressDropdownCreator = (args: {
|
||||
|
@ -701,7 +700,8 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
|
|||
})
|
||||
actionButtons.push({
|
||||
label: "move_action",
|
||||
click: (_: MouseEvent, dom: HTMLElement) => showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
|
||||
click: (_: MouseEvent, dom: HTMLElement) =>
|
||||
showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
|
||||
icon: Icons.Folder,
|
||||
})
|
||||
actionButtons.push({
|
||||
|
@ -733,7 +733,7 @@ export class MailViewerHeader implements Component<MailViewerHeaderAttrs> {
|
|||
actionButtons.push({
|
||||
label: "move_action",
|
||||
click: (_: MouseEvent, dom: HTMLElement) =>
|
||||
showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
|
||||
showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail]),
|
||||
icon: Icons.Folder,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import m, { Children, Component, Vnode } from "mithril"
|
||||
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { Mail } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { IconButton } from "../../../common/gui/base/IconButton.js"
|
||||
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
|
||||
|
@ -21,11 +21,13 @@ import { ColumnWidth, Table } from "../../../common/gui/base/Table.js"
|
|||
import { ExpanderButton, ExpanderPanel } from "../../../common/gui/base/Expander.js"
|
||||
import stream from "mithril/stream"
|
||||
import { exportMails } from "../export/Exporter.js"
|
||||
import { MailModel } from "../model/MailModel.js"
|
||||
|
||||
/*
|
||||
note that mailViewerViewModel has a mailModel, so you do not need to pass both if you pass a mailViewerViewModel
|
||||
*/
|
||||
export interface MailViewerToolbarAttrs {
|
||||
mailboxModel: MailboxModel
|
||||
mailModel: MailModel
|
||||
mailViewerViewModel?: MailViewerViewModel
|
||||
mails: Mail[]
|
||||
|
@ -50,13 +52,13 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
|
|||
} else if (attrs.mailViewerViewModel) {
|
||||
return [
|
||||
this.renderDeleteButton(mailModel, attrs.mails, attrs.selectNone ?? noOp),
|
||||
attrs.mailViewerViewModel.canForwardOrMove() ? this.renderMoveButton(mailModel, attrs.mails) : null,
|
||||
attrs.mailViewerViewModel.canForwardOrMove() ? this.renderMoveButton(attrs.mailboxModel, mailModel, attrs.mails) : null,
|
||||
attrs.mailViewerViewModel.isDraftMail() ? null : this.renderReadButton(attrs),
|
||||
]
|
||||
} else if (attrs.mails.length > 0) {
|
||||
return [
|
||||
this.renderDeleteButton(mailModel, attrs.mails, attrs.selectNone ?? noOp),
|
||||
attrs.mailModel.isMovingMailsAllowed() ? this.renderMoveButton(mailModel, attrs.mails) : null,
|
||||
attrs.mailModel.isMovingMailsAllowed() ? this.renderMoveButton(attrs.mailboxModel, mailModel, attrs.mails) : null,
|
||||
this.renderReadButton(attrs),
|
||||
this.renderExportButton(attrs),
|
||||
]
|
||||
|
@ -94,11 +96,11 @@ export class MailViewerActions implements Component<MailViewerToolbarAttrs> {
|
|||
})
|
||||
}
|
||||
|
||||
private renderMoveButton(mailModel: MailModel, mails: Mail[]): Children {
|
||||
private renderMoveButton(mailboxModel: MailboxModel, mailModel: MailModel, mails: Mail[]): Children {
|
||||
return m(IconButton, {
|
||||
title: "move_action",
|
||||
icon: Icons.Folder,
|
||||
click: (e, dom) => showMoveMailsDropdown(mailModel, dom.getBoundingClientRect(), mails),
|
||||
click: (e, dom) => showMoveMailsDropdown(mailboxModel, mailModel, dom.getBoundingClientRect(), mails),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
OperationType,
|
||||
} from "../../../common/api/common/TutanotaConstants"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient"
|
||||
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { ContactModel } from "../../../common/contactsFunctionality/ContactModel.js"
|
||||
import { ConfigurationDatabase } from "../../../common/api/worker/facades/lazy/ConfigurationDatabase.js"
|
||||
import stream from "mithril/stream"
|
||||
|
@ -43,7 +43,7 @@ import { LoginController } from "../../../common/api/main/LoginController"
|
|||
import m from "mithril"
|
||||
import { LockedError, NotAuthorizedError, NotFoundError } from "../../../common/api/common/error/RestError"
|
||||
import { haveSameId, isSameId } from "../../../common/api/common/utils/EntityUtils"
|
||||
import { getReferencedAttachments, loadInlineImages, moveMails } from "./MailGuiUtils"
|
||||
import { getReferencedAttachments, isTutanotaTeamMail, loadInlineImages, moveMails } from "./MailGuiUtils"
|
||||
import { SanitizedFragment } from "../../../common/misc/HtmlSanitizer"
|
||||
import { CALENDAR_MIME_TYPE, FileController } from "../../../common/file/FileController"
|
||||
import { exportMails } from "../export/Exporter.js"
|
||||
|
@ -69,20 +69,19 @@ import { AttachmentType, getAttachmentType } from "../../../common/gui/Attachmen
|
|||
import type { ContactImporter } from "../../contacts/ContactImporter.js"
|
||||
import { InlineImages, revokeInlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
|
||||
import {
|
||||
assertSystemFolderOfType,
|
||||
getDefaultSender,
|
||||
getEnabledMailAddressesWithUser,
|
||||
getFolderName,
|
||||
getMailboxName,
|
||||
getPathToFolderString,
|
||||
isNoReplyTeamAddress,
|
||||
isSystemNotification,
|
||||
isTutanotaTeamMail,
|
||||
loadMailDetails,
|
||||
loadMailHeaders,
|
||||
} from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSystemNotification, isNoReplyTeamAddress } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getDisplayedSender, getMailBodyText, MailAddressAndName } from "../../../common/api/common/CommonMailUtils.js"
|
||||
import { assertSystemFolderOfType, MailModel } from "../model/MailModel.js"
|
||||
import { CalendarModel } from "../../../calendar-app/calendar/model/CalendarModel.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
export const enum ContentBlockingStatus {
|
||||
Block = "0",
|
||||
|
@ -137,6 +136,7 @@ export class MailViewerViewModel {
|
|||
private _mail: Mail,
|
||||
showFolder: boolean,
|
||||
readonly entityClient: EntityClient,
|
||||
public readonly mailboxModel: MailboxModel,
|
||||
public readonly mailModel: MailModel,
|
||||
readonly contactModel: ContactModel,
|
||||
private readonly configFacade: ConfigurationDatabase,
|
||||
|
@ -149,7 +149,6 @@ export class MailViewerViewModel {
|
|||
private readonly mailFacade: MailFacade,
|
||||
private readonly cryptoFacade: CryptoFacade,
|
||||
private readonly contactImporter: lazyAsync<ContactImporter>,
|
||||
private readonly calendarModel: lazyAsync<CalendarModel>,
|
||||
) {
|
||||
this.folderMailboxText = null
|
||||
if (showFolder) {
|
||||
|
@ -206,10 +205,11 @@ export class MailViewerViewModel {
|
|||
|
||||
if (folder) {
|
||||
this.mailModel.getMailboxDetailsForMail(this.mail).then((mailboxDetails) => {
|
||||
if (mailboxDetails == null) {
|
||||
if (mailboxDetails == null || mailboxDetails.mailbox.folders == null) {
|
||||
return
|
||||
}
|
||||
const name = getPathToFolderString(mailboxDetails.folders, folder)
|
||||
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetails.mailbox.folders._id)
|
||||
const name = getPathToFolderString(folders, folder)
|
||||
this.folderMailboxText = `${getMailboxName(this.logins, mailboxDetails)} / ${name}`
|
||||
m.redraw()
|
||||
})
|
||||
|
@ -513,12 +513,19 @@ export class MailViewerViewModel {
|
|||
await this.entityClient.update(this.mail)
|
||||
}
|
||||
const mailboxDetail = await this.mailModel.getMailboxDetailsForMail(this.mail)
|
||||
if (mailboxDetail == null) {
|
||||
if (mailboxDetail == null || mailboxDetail.mailbox.folders == null) {
|
||||
return
|
||||
}
|
||||
const spamFolder = assertSystemFolderOfType(mailboxDetail.folders, MailSetKind.SPAM)
|
||||
const folders = this.mailModel.getMailboxFoldersForId(mailboxDetail.mailbox.folders._id)
|
||||
const spamFolder = assertSystemFolderOfType(folders, MailSetKind.SPAM)
|
||||
// do not report moved mails again
|
||||
await moveMails({ mailModel: this.mailModel, mails: [this.mail], targetMailFolder: spamFolder, isReportable: false })
|
||||
await moveMails({
|
||||
mailboxModel: this.mailboxModel,
|
||||
mailModel: this.mailModel,
|
||||
mails: [this.mail],
|
||||
targetMailFolder: spamFolder,
|
||||
isReportable: false,
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof NotFoundError) {
|
||||
console.log("mail already moved")
|
||||
|
@ -1055,7 +1062,7 @@ export class MailViewerViewModel {
|
|||
const { importCalendarFile, parseCalendarFile } = await import("../../../common/calendar/import/CalendarImporter.js")
|
||||
const dataFile = await this.fileController.getAsDataFile(file)
|
||||
const data = parseCalendarFile(dataFile)
|
||||
await importCalendarFile(await this.calendarModel(), this.logins.getUserController(), data.contents)
|
||||
await importCalendarFile(await mailLocator.calendarModel(), this.logins.getUserController(), data.contents)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw new UserError("errorDuringFileOpen_msg")
|
||||
|
|
|
@ -16,6 +16,7 @@ import { noOp, promiseMap } from "@tutao/tutanota-utils"
|
|||
import { EntityUpdateData, isUpdateForTypeRef } from "../../../common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { EntityEventsListener, EventController } from "../../../common/api/main/EventController.js"
|
||||
import { IconButton } from "../../../common/gui/base/IconButton.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
const COUNTER_POS_OFFSET = px(-8)
|
||||
export type MinimizedEditorOverlayAttrs = {
|
||||
|
@ -107,7 +108,7 @@ export class MinimizedEditorOverlay implements Component<MinimizedEditorOverlayA
|
|||
const draft = model.draft
|
||||
|
||||
if (draft) {
|
||||
await promptAndDeleteMails(model.mailModel, [draft], noOp)
|
||||
await promptAndDeleteMails(mailLocator.mailModel, [draft], noOp)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -56,7 +56,7 @@ export class MobileMailActionBar implements Component<MobileMailActionBarAttrs>
|
|||
return m(IconButton, {
|
||||
title: "move_action",
|
||||
click: (e, dom) =>
|
||||
showMoveMailsDropdown(viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail], {
|
||||
showMoveMailsDropdown(viewModel.mailboxModel, viewModel.mailModel, dom.getBoundingClientRect(), [viewModel.mail], {
|
||||
width: this.dropdownWidth(),
|
||||
withBackground: true,
|
||||
}),
|
||||
|
|
|
@ -5,11 +5,13 @@ import { Icons } from "../../../common/gui/base/icons/Icons.js"
|
|||
import { promptAndDeleteMails, showMoveMailsDropdown } from "./MailGuiUtils.js"
|
||||
import { DROPDOWN_MARGIN } from "../../../common/gui/base/Dropdown.js"
|
||||
import { MobileBottomActionBar } from "../../../common/gui/MobileBottomActionBar.js"
|
||||
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { MailModel } from "../model/MailModel.js"
|
||||
|
||||
export interface MobileMailMultiselectionActionBarAttrs {
|
||||
mails: readonly Mail[]
|
||||
mailModel: MailModel
|
||||
mailboxModel: MailboxModel
|
||||
selectNone: () => unknown
|
||||
}
|
||||
|
||||
|
@ -17,7 +19,7 @@ export class MobileMailMultiselectionActionBar {
|
|||
private dom: HTMLElement | null = null
|
||||
|
||||
view({ attrs }: Vnode<MobileMailMultiselectionActionBarAttrs>): Children {
|
||||
const { mails, selectNone, mailModel } = attrs
|
||||
const { mails, selectNone, mailModel, mailboxModel } = attrs
|
||||
return m(
|
||||
MobileBottomActionBar,
|
||||
{
|
||||
|
@ -35,7 +37,7 @@ export class MobileMailMultiselectionActionBar {
|
|||
title: "move_action",
|
||||
click: (e, dom) => {
|
||||
const referenceDom = this.dom ?? dom
|
||||
showMoveMailsDropdown(mailModel, referenceDom.getBoundingClientRect(), mails, {
|
||||
showMoveMailsDropdown(mailboxModel, mailModel, referenceDom.getBoundingClientRect(), mails, {
|
||||
onSelected: () => selectNone,
|
||||
width: referenceDom.offsetWidth - DROPDOWN_MARGIN * 2,
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { assertMainOrNode, isAndroidApp, isApp, isBrowser, isDesktop, isElectronClient, isIOSApp, isTest } from "../common/api/common/Env.js"
|
||||
import { EventController } from "../common/api/main/EventController.js"
|
||||
import { SearchModel } from "./search/model/SearchModel.js"
|
||||
import { MailboxDetail, MailModel } from "../common/mailFunctionality/MailModel.js"
|
||||
import { type MailboxDetail, MailboxModel } from "../common/mailFunctionality/MailboxModel.js"
|
||||
import { MinimizedMailEditorViewModel } from "./mail/model/MinimizedMailEditorViewModel.js"
|
||||
import { ContactModel } from "../common/contactsFunctionality/ContactModel.js"
|
||||
import { EntityClient } from "../common/api/common/EntityClient.js"
|
||||
|
@ -64,7 +64,7 @@ import { SearchViewModel } from "./search/view/SearchViewModel.js"
|
|||
import { SearchRouter } from "../common/search/view/SearchRouter.js"
|
||||
import { MailOpenedListener } from "./mail/view/MailViewModel.js"
|
||||
import { getEnabledMailAddressesWithUser } from "../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { Const, FeatureType, GroupType, KdfType } from "../common/api/common/TutanotaConstants.js"
|
||||
import { Const, FeatureType, GroupType, KdfType, MailSetKind } from "../common/api/common/TutanotaConstants.js"
|
||||
import { ShareableGroupType } from "../common/sharing/GroupUtils.js"
|
||||
import { ReceivedGroupInvitationsModel } from "../common/sharing/model/ReceivedGroupInvitationsModel.js"
|
||||
import { CalendarViewModel } from "../calendar-app/calendar/view/CalendarViewModel.js"
|
||||
|
@ -116,17 +116,19 @@ import { MobilePaymentsFacade } from "../common/native/common/generatedipc/Mobil
|
|||
import { AppStorePaymentPicker } from "../common/misc/AppStorePaymentPicker.js"
|
||||
import { MAIL_PREFIX } from "../common/misc/RouteChange.js"
|
||||
import { getDisplayedSender } from "../common/api/common/CommonMailUtils.js"
|
||||
import { assertSystemFolderOfType, isMailInSpamOrTrash, MailModel } from "./mail/model/MailModel.js"
|
||||
import { AppType } from "../common/misc/ClientConstants.js"
|
||||
import type { ParsedEvent } from "../common/calendar/import/CalendarImporter.js"
|
||||
import type { ContactImporter } from "./contacts/ContactImporter.js"
|
||||
import { ExternalCalendarFacade } from "../common/native/common/generatedipc/ExternalCalendarFacade.js"
|
||||
import { locator } from "../common/api/main/CommonLocator.js"
|
||||
import m from "mithril"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
class MailLocator {
|
||||
eventController!: EventController
|
||||
search!: SearchModel
|
||||
mailboxModel!: MailboxModel
|
||||
mailModel!: MailModel
|
||||
minimizedMailModel!: MinimizedMailEditorViewModel
|
||||
contactModel!: ContactModel
|
||||
|
@ -224,6 +226,7 @@ class MailLocator {
|
|||
const conversationViewModelFactory = await this.conversationViewModelFactory()
|
||||
const router = new ScopedRouter(this.throttledRouter(), "/mail")
|
||||
return new MailViewModel(
|
||||
this.mailboxModel,
|
||||
this.mailModel,
|
||||
this.entityClient,
|
||||
this.eventController,
|
||||
|
@ -257,7 +260,7 @@ class MailLocator {
|
|||
searchRouter,
|
||||
this.search,
|
||||
this.searchFacade,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.logins,
|
||||
this.indexerFacade,
|
||||
this.entityClient,
|
||||
|
@ -325,8 +328,8 @@ class MailLocator {
|
|||
return new CalendarViewModel(
|
||||
this.logins,
|
||||
async (mode: CalendarOperation, event: CalendarEvent) => {
|
||||
const mailboxDetail = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
|
||||
const mailboxDetail = await this.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetail.mailboxGroupRoot)
|
||||
return await this.calendarEventModel(mode, event, mailboxDetail, mailboxProperties, null)
|
||||
},
|
||||
(...args) => this.calendarEventPreviewModel(...args),
|
||||
|
@ -338,7 +341,7 @@ class MailLocator {
|
|||
deviceConfig,
|
||||
await this.receivedGroupInvitationsModel(GroupType.Calendar),
|
||||
timeZone,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -359,13 +362,16 @@ class MailLocator {
|
|||
this.mailFacade,
|
||||
this.entityClient,
|
||||
this.logins,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.contactModel,
|
||||
this.eventController,
|
||||
mailboxDetails,
|
||||
recipientsModel,
|
||||
dateProvider,
|
||||
mailboxProperties,
|
||||
async (mail: Mail) => {
|
||||
return await isMailInSpamOrTrash(mail)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -443,13 +449,14 @@ class MailLocator {
|
|||
mail,
|
||||
showFolder,
|
||||
this.entityClient,
|
||||
this.mailboxModel,
|
||||
this.mailModel,
|
||||
this.contactModel,
|
||||
this.configFacade,
|
||||
this.fileController,
|
||||
this.logins,
|
||||
async (mailboxDetails) => {
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
return this.sendMailModel(mailboxDetails, mailboxProperties)
|
||||
},
|
||||
this.eventController,
|
||||
|
@ -458,7 +465,6 @@ class MailLocator {
|
|||
this.mailFacade,
|
||||
this.cryptoFacade,
|
||||
() => this.contactImporter(),
|
||||
() => this.calendarModel(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -539,7 +545,7 @@ class MailLocator {
|
|||
|
||||
async ownMailAddressNameChanger(): Promise<MailAddressNameChanger> {
|
||||
const { OwnMailAddressNameChanger } = await import("../mail-app/settings/mailaddress/OwnMailAddressNameChanger.js")
|
||||
return new OwnMailAddressNameChanger(this.mailModel, this.entityClient)
|
||||
return new OwnMailAddressNameChanger(this.mailboxModel, this.entityClient)
|
||||
}
|
||||
|
||||
async adminNameChanger(mailGroupId: Id, userId: Id): Promise<MailAddressNameChanger> {
|
||||
|
@ -685,12 +691,14 @@ class MailLocator {
|
|||
this.entropyFacade = entropyFacade
|
||||
this.workerFacade = workerFacade
|
||||
this.connectivityModel = new WebsocketConnectivityModel(eventBus)
|
||||
this.mailboxModel = new MailboxModel(this.eventController, this.entityClient, this.logins)
|
||||
this.mailModel = new MailModel(
|
||||
notifications,
|
||||
this.mailboxModel,
|
||||
this.eventController,
|
||||
this.mailFacade,
|
||||
this.entityClient,
|
||||
this.logins,
|
||||
this.mailFacade,
|
||||
this.connectivityModel,
|
||||
this.inboxRuleHanlder(),
|
||||
)
|
||||
|
@ -729,18 +737,63 @@ class MailLocator {
|
|||
const { WebAuthnFacadeSendDispatcher } = await import("../common/native/common/generatedipc/WebAuthnFacadeSendDispatcher.js")
|
||||
const { createNativeInterfaces, createDesktopInterfaces } = await import("../common/native/main/NativeInterfaceFactory.js")
|
||||
|
||||
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailModel, MAIL_PREFIX)
|
||||
this.webMobileFacade = new WebMobileFacade(this.connectivityModel, this.mailboxModel, MAIL_PREFIX, async (currentRoute: string) => {
|
||||
// If the first background column is focused in mail view (showing a folder), move to inbox.
|
||||
// If in inbox already, quit
|
||||
const parts = currentRoute.split("/").filter((part) => part !== "")
|
||||
|
||||
if (parts.length > 1) {
|
||||
const selectedMailListId = parts[1]
|
||||
const [mailboxDetail] = await this.mailboxModel.getMailboxDetails()
|
||||
const folders = this.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
const inboxMailListId = assertSystemFolderOfType(folders, MailSetKind.INBOX).mails
|
||||
|
||||
if (inboxMailListId !== selectedMailListId) {
|
||||
return MAIL_PREFIX + "/" + inboxMailListId
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
this.nativeInterfaces = createNativeInterfaces(
|
||||
this.webMobileFacade,
|
||||
new WebDesktopFacade(this.logins, async () => this.native),
|
||||
new WebInterWindowEventFacade(this.logins, windowFacade, deviceConfig),
|
||||
new WebCommonNativeFacade(
|
||||
this.logins,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.usageTestController,
|
||||
async () => this.fileApp,
|
||||
async () => this.pushService,
|
||||
this.handleFileImport.bind(this),
|
||||
async (userId: string, mailAddress: string, requestedPath: string | null) => {
|
||||
if (mailLocator.logins.isUserLoggedIn() && mailLocator.logins.getUserController().user._id === userId) {
|
||||
if (!requestedPath) {
|
||||
const [mailboxDetail] = await mailLocator.mailboxModel.getMailboxDetails()
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
const inbox = assertSystemFolderOfType(folders, MailSetKind.INBOX)
|
||||
m.route.set("/mail/" + inbox.mails)
|
||||
} else {
|
||||
m.route.set("/mail" + requestedPath)
|
||||
}
|
||||
} else {
|
||||
if (!requestedPath) {
|
||||
m.route.set(`/login?noAutoLogin=false&userId=${userId}&loginWith=${mailAddress}`)
|
||||
} else {
|
||||
m.route.set(
|
||||
`/login?noAutoLogin=false&userId=${userId}&loginWith=${mailAddress}&requestedPath=${encodeURIComponent(requestedPath)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
async (userId: string) => {
|
||||
if (mailLocator.logins.isUserLoggedIn() && mailLocator.logins.getUserController().user._id === userId) {
|
||||
m.route.set("/calendar/agenda")
|
||||
} else {
|
||||
m.route.set(`/login?noAutoLogin=false&userId=${userId}&requestedPath=${encodeURIComponent("/calendar/agenda")}`)
|
||||
}
|
||||
},
|
||||
AppType.Integrated,
|
||||
),
|
||||
cryptoFacade,
|
||||
|
@ -861,7 +914,7 @@ class MailLocator {
|
|||
this.logins,
|
||||
this.progressTracker,
|
||||
this.entityClient,
|
||||
this.mailModel,
|
||||
this.mailboxModel,
|
||||
this.calendarFacade,
|
||||
this.fileController,
|
||||
timeZone,
|
||||
|
@ -873,7 +926,7 @@ class MailLocator {
|
|||
readonly calendarInviteHandler: () => Promise<CalendarInviteHandler> = lazyMemoized(async () => {
|
||||
const { CalendarInviteHandler } = await import("../calendar-app/calendar/view/CalendarInvites.js")
|
||||
const { calendarNotificationSender } = await import("../calendar-app/calendar/view/CalendarNotificationSender.js")
|
||||
return new CalendarInviteHandler(this.mailModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
|
||||
return new CalendarInviteHandler(this.mailboxModel, await this.calendarModel(), this.logins, calendarNotificationSender, (...arg) =>
|
||||
this.sendMailModel(...arg),
|
||||
)
|
||||
})
|
||||
|
@ -938,9 +991,9 @@ class MailLocator {
|
|||
const { getEventType } = await import("../calendar-app/calendar/gui/CalendarGuiUtils.js")
|
||||
const { CalendarEventPreviewViewModel } = await import("../calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.js")
|
||||
|
||||
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
|
||||
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
|
||||
const userController = this.logins.getUserController()
|
||||
const customer = await userController.loadCustomer()
|
||||
|
@ -989,7 +1042,7 @@ class MailLocator {
|
|||
mailLocator.nativeContactsSyncManager()?.syncContacts()
|
||||
},
|
||||
async () => {
|
||||
const calendarModel = await locator.calendarModel()
|
||||
const calendarModel = await mailLocator.calendarModel()
|
||||
calendarModel.handleSyncExternalCalendars()
|
||||
},
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Icon } from "../../common/gui/base/Icon"
|
|||
import { client } from "../../common/misc/ClientDetector"
|
||||
import m, { Children, Component, Vnode } from "mithril"
|
||||
import { theme } from "../../common/gui/theme"
|
||||
import { getMailFolderIcon } from "../mail/view/MailGuiUtils"
|
||||
import { getMailFolderIcon, isTutanotaTeamMail } from "../mail/view/MailGuiUtils"
|
||||
import { locator } from "../../common/api/main/CommonLocator"
|
||||
import { IndexingErrorReason } from "../../common/api/worker/search/SearchTypes"
|
||||
import { companyTeamLabel } from "../../common/misc/ClientConstants.js"
|
||||
|
@ -21,7 +21,6 @@ import { getTimeZone } from "../../common/calendar/date/CalendarUtils.js"
|
|||
|
||||
import { formatEventDuration } from "../../calendar-app/calendar/gui/CalendarGuiUtils.js"
|
||||
import { getSenderOrRecipientHeading } from "../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isTutanotaTeamMail } from "../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getContactListName } from "../../common/contactsFunctionality/ContactUtils.js"
|
||||
|
||||
type SearchBarOverlayAttrs = {
|
||||
|
|
|
@ -103,6 +103,7 @@ import { getSharedGroupName } from "../../../common/sharing/GroupUtils.js"
|
|||
import { YEAR_IN_MILLIS } from "@tutao/tutanota-utils/dist/DateUtils.js"
|
||||
import { getIndentedFolderNameForDropdown } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { BottomNav } from "../../gui/BottomNav.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -402,7 +403,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
const conversationViewModel = this.searchViewModel.conversationViewModel
|
||||
if (this.searchViewModel.listModel?.state.inMultiselect || !conversationViewModel) {
|
||||
const actions = m(MailViewerActions, {
|
||||
mailModel: locator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mails: selectedMails,
|
||||
selectNone: () => this.searchViewModel.listModel.selectNone(),
|
||||
})
|
||||
|
@ -435,6 +437,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
})
|
||||
} else {
|
||||
const actions = m(MailViewerActions, {
|
||||
mailboxModel: conversationViewModel.primaryViewModel().mailboxModel,
|
||||
mailModel: conversationViewModel.primaryViewModel().mailModel,
|
||||
mailViewerViewModel: conversationViewModel.primaryViewModel(),
|
||||
mails: [conversationViewModel.primaryMail],
|
||||
|
@ -603,7 +606,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
return m(MobileMailMultiselectionActionBar, {
|
||||
mails: this.searchViewModel.getSelectedMails(),
|
||||
selectNone: () => this.searchViewModel.listModel.selectNone(),
|
||||
mailModel: locator.mailModel,
|
||||
mailModel: mailLocator.mailModel,
|
||||
mailboxModel: locator.mailboxModel,
|
||||
})
|
||||
} else if (this.viewSlider.focusedColumn === this.resultListColumn) {
|
||||
return m(
|
||||
|
@ -645,8 +649,9 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
]
|
||||
|
||||
for (const mailbox of mailboxes) {
|
||||
const folderStructures = mailLocator.mailModel.folders()
|
||||
const mailboxIndex = mailboxes.indexOf(mailbox)
|
||||
const mailFolders = mailbox.folders.getIndentedList()
|
||||
const mailFolders = folderStructures[assertNotNull(mailbox.mailbox.folders)._id].getIndentedList()
|
||||
for (const folderInfo of mailFolders) {
|
||||
if (folderInfo.folder.folderType !== MailSetKind.SPAM) {
|
||||
const mailboxLabel = mailboxIndex === 0 ? "" : ` (${getGroupInfoDisplayName(mailbox.mailGroupInfo)})`
|
||||
|
@ -990,8 +995,8 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
await showProgressDialog("pleaseWait_msg", calendarInfos)
|
||||
}
|
||||
|
||||
const mailboxDetails = await locator.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await locator.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxDetails = await locator.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const model = await locator.calendarEventModel(CalendarOperation.Create, getEventWithDefaultTimes(dateToUse), mailboxDetails, mailboxProperties, null)
|
||||
|
||||
if (model) {
|
||||
|
@ -1027,7 +1032,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
const selectedMails = this.searchViewModel.getSelectedMails()
|
||||
|
||||
if (selectedMails.length > 0) {
|
||||
showMoveMailsDropdown(locator.mailModel, getMoveMailBounds(), selectedMails, {
|
||||
showMoveMailsDropdown(locator.mailboxModel, mailLocator.mailModel, getMoveMailBounds(), selectedMails, {
|
||||
onSelected: () => {
|
||||
if (selectedMails.length > 1) {
|
||||
this.searchViewModel.listModel.selectNone()
|
||||
|
@ -1041,7 +1046,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
let selectedMails = this.searchViewModel.getSelectedMails()
|
||||
|
||||
if (selectedMails.length > 0) {
|
||||
locator.mailModel.markMails(selectedMails, !selectedMails[0].unread)
|
||||
mailLocator.mailModel.markMails(selectedMails, !selectedMails[0].unread)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1056,7 +1061,7 @@ export class SearchView extends BaseTopLevelView implements TopLevelView<SearchV
|
|||
this.searchViewModel.listModel.selectNone()
|
||||
}
|
||||
|
||||
locator.mailModel.deleteMails(selected)
|
||||
mailLocator.mailModel.deleteMails(selected)
|
||||
}
|
||||
})
|
||||
} else if (isSameTypeRef(this.searchViewModel.searchedType, ContactTypeRef)) {
|
||||
|
@ -1147,6 +1152,6 @@ function getCurrentSearchMode(): SearchCategoryTypes {
|
|||
}
|
||||
|
||||
async function newMailEditor(): Promise<Dialog> {
|
||||
const [mailboxDetails, { newMailEditor }] = await Promise.all([locator.mailModel.getUserMailboxDetails(), import("../../mail/editor/MailEditor")])
|
||||
const [mailboxDetails, { newMailEditor }] = await Promise.all([locator.mailboxModel.getUserMailboxDetails(), import("../../mail/editor/MailEditor")])
|
||||
return newMailEditor(mailboxDetails)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ import {
|
|||
SearchCategoryTypes,
|
||||
} from "../model/SearchUtils.js"
|
||||
import Stream from "mithril/stream"
|
||||
import { MailboxDetail, MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { SearchFacade } from "../../../common/api/worker/search/SearchFacade.js"
|
||||
import { LoginController } from "../../../common/api/main/LoginController.js"
|
||||
import { Indexer } from "../../../common/api/worker/search/Indexer.js"
|
||||
|
@ -64,6 +64,7 @@ import { ProgressTracker } from "../../../common/api/main/ProgressTracker.js"
|
|||
import { ListAutoSelectBehavior } from "../../../common/misc/DeviceConfig.js"
|
||||
import { getMailFilterForType, MailFilterType } from "../../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getStartOfTheWeekOffsetForUser } from "../../../common/calendar/date/CalendarUtils.js"
|
||||
import { mailLocator } from "../../mailLocator.js"
|
||||
|
||||
const SEARCH_PAGE_SIZE = 100
|
||||
|
||||
|
@ -124,7 +125,7 @@ export class SearchViewModel {
|
|||
readonly router: SearchRouter,
|
||||
private readonly search: SearchModel,
|
||||
private readonly searchFacade: SearchFacade,
|
||||
private readonly mailModel: MailModel,
|
||||
private readonly mailboxModel: MailboxModel,
|
||||
private readonly logins: LoginController,
|
||||
private readonly indexerFacade: Indexer,
|
||||
private readonly entityClient: EntityClient,
|
||||
|
@ -164,7 +165,7 @@ export class SearchViewModel {
|
|||
}
|
||||
})
|
||||
|
||||
this.mailboxSubscription = this.mailModel.mailboxDetails.map((mailboxes) => this.onMailboxesChanged(mailboxes))
|
||||
this.mailboxSubscription = this.mailboxModel.mailboxDetails.map((mailboxes) => this.onMailboxesChanged(mailboxes))
|
||||
this.eventController.addEntityListener(this.entityEventsListener)
|
||||
})
|
||||
|
||||
|
@ -552,11 +553,17 @@ export class SearchViewModel {
|
|||
|
||||
private onMailboxesChanged(mailboxes: MailboxDetail[]) {
|
||||
this.mailboxes = mailboxes
|
||||
const folderStructures = mailLocator.mailModel.folders()
|
||||
|
||||
// if selected folder no longer exist select another one
|
||||
const selectedMailFolder = this.selectedMailFolder
|
||||
if (selectedMailFolder[0] && mailboxes.every((mailbox) => mailbox.folders.getFolderById(selectedMailFolder[0]) == null)) {
|
||||
this.selectedMailFolder = [getElementId(assertNotNull(mailboxes[0].folders.getSystemFolderByType(MailSetKind.INBOX)))]
|
||||
if (
|
||||
selectedMailFolder[0] &&
|
||||
mailboxes.every((mailbox) => folderStructures[assertNotNull(mailbox.mailbox.folders)._id].getFolderById(selectedMailFolder[0]) == null)
|
||||
) {
|
||||
this.selectedMailFolder = [
|
||||
getElementId(assertNotNull(folderStructures[assertNotNull(mailboxes[0].mailbox.folders)._id].getSystemFolderByType(MailSetKind.INBOX))),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { isDomainName, isMailAddress, isRegularExpression } from "../../common/m
|
|||
import { getInboxRuleTypeNameMapping } from "../mail/model/InboxRuleHandler"
|
||||
import type { InboxRule } from "../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { createInboxRule } from "../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import type { MailboxDetail } from "../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../common/mailFunctionality/MailboxModel.js"
|
||||
import stream from "mithril/stream"
|
||||
import { DropDownSelector } from "../../common/gui/base/DropDownSelector.js"
|
||||
import { TextField } from "../../common/gui/base/TextField.js"
|
||||
|
@ -18,12 +18,14 @@ import { assertMainOrNode } from "../../common/api/common/Env"
|
|||
import { locator } from "../../common/api/main/CommonLocator"
|
||||
import { isOfflineError } from "../../common/api/common/utils/ErrorUtils.js"
|
||||
import {
|
||||
assertSystemFolderOfType,
|
||||
getExistingRuleForType,
|
||||
getFolderName,
|
||||
getIndentedFolderNameForDropdown,
|
||||
getPathToFolderString,
|
||||
} from "../../common/mailFunctionality/SharedMailUtils.js"
|
||||
import { assertSystemFolderOfType } from "../mail/model/MailModel.js"
|
||||
import { mailLocator } from "../mailLocator.js"
|
||||
import type { IndentedFolder } from "../../common/api/common/mail/FolderSystem.js"
|
||||
|
||||
assertMainOrNode()
|
||||
|
||||
|
@ -32,8 +34,9 @@ export type InboxRuleTemplate = Pick<InboxRule, "type" | "value"> & { _id?: Inbo
|
|||
export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemplate) {
|
||||
if (locator.logins.getUserController().isFreeAccount()) {
|
||||
showNotAvailableForFreeDialog()
|
||||
} else if (mailBoxDetail) {
|
||||
let targetFolders = mailBoxDetail.folders.getIndentedList().map((folderInfo) => {
|
||||
} else if (mailBoxDetail && mailBoxDetail.mailbox.folders) {
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(mailBoxDetail.mailbox.folders._id)
|
||||
let targetFolders = folders.getIndentedList().map((folderInfo: IndentedFolder) => {
|
||||
return {
|
||||
name: getIndentedFolderNameForDropdown(folderInfo),
|
||||
value: folderInfo.folder,
|
||||
|
@ -41,8 +44,8 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
|
|||
})
|
||||
const inboxRuleType = stream(ruleOrTemplate.type)
|
||||
const inboxRuleValue = stream(ruleOrTemplate.value)
|
||||
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : mailBoxDetail.folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder))
|
||||
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(mailBoxDetail.folders, MailSetKind.ARCHIVE))
|
||||
const selectedFolder = ruleOrTemplate.targetFolder == null ? null : folders.getFolderById(elementIdPart(ruleOrTemplate.targetFolder))
|
||||
const inboxRuleTarget = stream(selectedFolder ?? assertSystemFolderOfType(folders, MailSetKind.ARCHIVE))
|
||||
|
||||
let form = () => [
|
||||
m(DropDownSelector, {
|
||||
|
@ -66,7 +69,7 @@ export function show(mailBoxDetail: MailboxDetail, ruleOrTemplate: InboxRuleTemp
|
|||
selectedValue: inboxRuleTarget(),
|
||||
selectedValueDisplay: getFolderName(inboxRuleTarget()),
|
||||
selectionChangedHandler: inboxRuleTarget,
|
||||
helpLabel: () => getPathToFolderString(mailBoxDetail.folders, inboxRuleTarget(), true),
|
||||
helpLabel: () => getPathToFolderString(folders, inboxRuleTarget(), true),
|
||||
}),
|
||||
]
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ import {
|
|||
TutanotaPropertiesTypeRef,
|
||||
} from "../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { Const, FeatureType, InboxRuleType, OperationType, ReportMovedMailsType } from "../../common/api/common/TutanotaConstants"
|
||||
import { capitalizeFirstLetter, defer, LazyLoaded, noOp, ofClass } from "@tutao/tutanota-utils"
|
||||
import { assertNotNull, capitalizeFirstLetter, defer, LazyLoaded, noOp, ofClass } from "@tutao/tutanota-utils"
|
||||
import { getInboxRuleTypeName } from "../mail/model/InboxRuleHandler"
|
||||
import { MailAddressTable } from "../../common/settings/mailaddress/MailAddressTable.js"
|
||||
import { Dialog } from "../../common/gui/base/Dialog"
|
||||
import { Icons } from "../../common/gui/base/icons/Icons"
|
||||
import { showProgressDialog } from "../../common/gui/dialogs/ProgressDialog"
|
||||
import type { MailboxDetail } from "../../common/mailFunctionality/MailModel.js"
|
||||
import type { MailboxDetail } from "../../common/mailFunctionality/MailboxModel.js"
|
||||
import { locator } from "../../common/api/main/CommonLocator"
|
||||
import stream from "mithril/stream"
|
||||
import Stream from "mithril/stream"
|
||||
|
@ -98,7 +98,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
|
|||
|
||||
this._mailboxProperties = new LazyLoaded(async () => {
|
||||
const mailboxGroupRoot = await this.getMailboxGroupRoot()
|
||||
return mailLocator.mailModel.getMailboxProperties(mailboxGroupRoot)
|
||||
return mailLocator.mailboxModel.getMailboxProperties(mailboxGroupRoot)
|
||||
})
|
||||
|
||||
this._updateMailboxPropertiesSettings()
|
||||
|
@ -120,7 +120,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
|
|||
|
||||
private async getMailboxGroupRoot(): Promise<MailboxGroupRoot> {
|
||||
// For now we assume user mailbox, in the future we should specify which mailbox we are configuring
|
||||
const { mailboxGroupRoot } = await mailLocator.mailModel.getUserMailboxDetails()
|
||||
const { mailboxGroupRoot } = await mailLocator.mailboxModel.getUserMailboxDetails()
|
||||
return mailboxGroupRoot
|
||||
}
|
||||
|
||||
|
@ -283,7 +283,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
|
|||
const templateRule = createInboxRuleTemplate(InboxRuleType.RECIPIENT_TO_EQUALS, "")
|
||||
const addInboxRuleButtonAttrs: IconButtonAttrs = {
|
||||
title: "addInboxRule_action",
|
||||
click: () => mailLocator.mailModel.getUserMailboxDetails().then((mailboxDetails) => AddInboxRuleDialog.show(mailboxDetails, templateRule)),
|
||||
click: () => mailLocator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => AddInboxRuleDialog.show(mailboxDetails, templateRule)),
|
||||
icon: Icons.Add,
|
||||
size: ButtonSize.Compact,
|
||||
}
|
||||
|
@ -444,7 +444,7 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
|
|||
}
|
||||
|
||||
_updateInboxRules(props: TutanotaProperties): void {
|
||||
mailLocator.mailModel.getUserMailboxDetails().then((mailboxDetails) => {
|
||||
mailLocator.mailboxModel.getUserMailboxDetails().then((mailboxDetails) => {
|
||||
this._inboxRulesTableLines(
|
||||
props.inboxRules.map((rule, index) => {
|
||||
return {
|
||||
|
@ -480,7 +480,8 @@ export class MailSettingsViewer implements UpdatableSettingsViewer {
|
|||
}
|
||||
|
||||
_getTextForTarget(mailboxDetail: MailboxDetail, targetFolderId: IdTuple): string {
|
||||
let folder = mailboxDetail.folders.getFolderById(elementIdPart(targetFolderId))
|
||||
const folders = mailLocator.mailModel.getMailboxFoldersForId(assertNotNull(mailboxDetail.mailbox.folders)._id)
|
||||
let folder = folders.getFolderById(elementIdPart(targetFolderId))
|
||||
|
||||
if (folder) {
|
||||
return getFolderName(folder)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { AddressToName, MailAddressNameChanger } from "../../../common/settings/mailaddress/MailAddressTableModel.js"
|
||||
import { MailModel } from "../../../common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
|
||||
import { createMailAddressProperties, MailboxProperties } from "../../../common/api/entities/tutanota/TypeRefs.js"
|
||||
import { EntityClient } from "../../../common/api/common/EntityClient.js"
|
||||
import { findAndRemove } from "@tutao/tutanota-utils"
|
||||
|
||||
/** Name changer for personal mailbox of the currently logged-in user. */
|
||||
export class OwnMailAddressNameChanger implements MailAddressNameChanger {
|
||||
constructor(private readonly mailModel: MailModel, private readonly entityClient: EntityClient) {}
|
||||
constructor(private readonly mailboxModel: MailboxModel, private readonly entityClient: EntityClient) {}
|
||||
|
||||
async getSenderNames(): Promise<AddressToName> {
|
||||
const mailboxProperties = await this.getMailboxProperties()
|
||||
|
@ -14,8 +14,8 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
|
|||
}
|
||||
|
||||
async setSenderName(address: string, name: string): Promise<AddressToName> {
|
||||
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
let aliasConfig = mailboxProperties.mailAddressProperties.find((p) => p.mailAddress === address)
|
||||
if (aliasConfig == null) {
|
||||
aliasConfig = createMailAddressProperties({ mailAddress: address, senderName: name })
|
||||
|
@ -28,8 +28,8 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
|
|||
}
|
||||
|
||||
async removeSenderName(address: string): Promise<AddressToName> {
|
||||
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
|
||||
const mailboxProperties = await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
findAndRemove(mailboxProperties.mailAddressProperties, (p) => p.mailAddress === address)
|
||||
await this.entityClient.update(mailboxProperties)
|
||||
return this.collectMap(mailboxProperties)
|
||||
|
@ -44,7 +44,7 @@ export class OwnMailAddressNameChanger implements MailAddressNameChanger {
|
|||
}
|
||||
|
||||
private async getMailboxProperties(): Promise<MailboxProperties> {
|
||||
const mailboxDetails = await this.mailModel.getUserMailboxDetails()
|
||||
return await this.mailModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
const mailboxDetails = await this.mailboxModel.getUserMailboxDetails()
|
||||
return await this.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,9 @@ import { createTestEntity } from "../../../TestUtils.js"
|
|||
import { Icons } from "../../../../../src/common/gui/base/icons/Icons.js"
|
||||
import { ProgrammingError } from "../../../../../src/common/api/common/error/ProgrammingError.js"
|
||||
import { getConfidentialIcon } from "../../../../../src/common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSystemNotification, isTutanotaTeamAddress, isTutanotaTeamMail } from "../../../../../src/common/mailFunctionality/SharedMailUtils.js"
|
||||
import { isSystemNotification } from "../../../../../src/common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getDisplayedSender } from "../../../../../src/common/api/common/CommonMailUtils.js"
|
||||
import { isTutanotaTeamAddress, isTutanotaTeamMail } from "../../../../../src/mail-app/mail/view/MailGuiUtils.js"
|
||||
|
||||
o.spec("MailUtilsTest", function () {
|
||||
function createSystemMail(overrides: Partial<Mail> = {}): Mail {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { getFromMap, remove } from "@tutao/tutanota-utils"
|
|||
class FakeWindow {
|
||||
listeners: Map<string, ((e: unknown) => unknown)[]> = new Map()
|
||||
|
||||
addEventListener: (typeof Window.prototype)["addEventListener"] = (event, listener) => {
|
||||
addEventListener: typeof Window.prototype["addEventListener"] = (event, listener) => {
|
||||
this.getListeners(event).push(listener)
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ class FakeWindow {
|
|||
return getFromMap(this.listeners, event, () => [])
|
||||
}
|
||||
|
||||
removeEventListener: (typeof Window.prototype)["removeEventListener"] = (event, listener) => {
|
||||
removeEventListener: typeof Window.prototype["removeEventListener"] = (event, listener) => {
|
||||
remove(this.getListeners(event), listener)
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ class FakeWindow {
|
|||
}
|
||||
}
|
||||
|
||||
crypto: Partial<(typeof Window.prototype)["crypto"]> = {
|
||||
crypto: Partial<typeof Window.prototype["crypto"]> = {
|
||||
getRandomValues<T extends ArrayBufferView | null>(array: T): T {
|
||||
if (array) {
|
||||
array[0] = 32
|
||||
|
|
|
@ -18,20 +18,25 @@ import { findAttendeeInAddresses } from "../../../src/common/api/common/utils/Co
|
|||
import { instance, matchers, when } from "testdouble"
|
||||
import { CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js"
|
||||
import { LoginController } from "../../../src/common/api/main/LoginController.js"
|
||||
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
|
||||
import { GroupInfoTypeRef, GroupTypeRef, User } from "../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { calendars, makeUserController } from "./CalendarTestUtils.js"
|
||||
import { UserController } from "../../../src/common/api/main/UserController.js"
|
||||
import { CalendarNotificationSender } from "../../../src/calendar-app/calendar/view/CalendarNotificationSender.js"
|
||||
import { mockAttribute } from "@tutao/tutanota-test-utils"
|
||||
import { SendMailModel } from "../../../src/common/mailFunctionality/SendMailModel.js"
|
||||
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
|
||||
|
||||
const { anything, argThat } = matchers
|
||||
|
||||
o.spec("CalendarInviteHandlerTest", function () {
|
||||
let mailModel: MailModel, calendarIniviteHandler: CalendarInviteHandler, calendarModel: CalendarModel, logins: LoginController, sendMailModel: SendMailModel
|
||||
let maiboxModel: MailboxModel,
|
||||
calendarIniviteHandler: CalendarInviteHandler,
|
||||
calendarModel: CalendarModel,
|
||||
logins: LoginController,
|
||||
sendMailModel: SendMailModel
|
||||
let calendarNotificationSender: CalendarNotificationSender
|
||||
let mailboxDetails: MailboxDetail
|
||||
|
||||
o.beforeEach(function () {
|
||||
const customerId = "customerId"
|
||||
|
@ -42,7 +47,7 @@ o.spec("CalendarInviteHandlerTest", function () {
|
|||
const userSettingsGroupRoot = createTestEntity(UserSettingsGroupRootTypeRef)
|
||||
let userController: Partial<UserController> = makeUserController([], AccountType.FREE, undefined, false, false, user, userSettingsGroupRoot)
|
||||
|
||||
const mailboxDetails: MailboxDetail = {
|
||||
mailboxDetails = {
|
||||
mailbox: createTestEntity(MailBoxTypeRef),
|
||||
folders: new FolderSystem([]),
|
||||
mailGroupInfo: createTestEntity(GroupInfoTypeRef, {
|
||||
|
@ -53,9 +58,8 @@ o.spec("CalendarInviteHandlerTest", function () {
|
|||
}
|
||||
const mailboxProperties: MailboxProperties = createTestEntity(MailboxPropertiesTypeRef, {})
|
||||
|
||||
mailModel = instance(MailModel)
|
||||
when(mailModel.getMailboxDetailsForMail(anything())).thenResolve(mailboxDetails)
|
||||
when(mailModel.getMailboxProperties(anything())).thenResolve(mailboxProperties)
|
||||
maiboxModel = instance(MailboxModel)
|
||||
when(maiboxModel.getMailboxProperties(anything())).thenResolve(mailboxProperties)
|
||||
|
||||
calendarModel = instance(CalendarModel)
|
||||
when(calendarModel.getEventsByUid(anything())).thenResolve({
|
||||
|
@ -73,7 +77,7 @@ o.spec("CalendarInviteHandlerTest", function () {
|
|||
|
||||
sendMailModel = instance(SendMailModel)
|
||||
|
||||
calendarIniviteHandler = new CalendarInviteHandler(mailModel, calendarModel, logins, calendarNotificationSender, async () => {
|
||||
calendarIniviteHandler = new CalendarInviteHandler(maiboxModel, calendarModel, logins, calendarNotificationSender, async () => {
|
||||
return sendMailModel
|
||||
})
|
||||
})
|
||||
|
@ -103,7 +107,9 @@ o.spec("CalendarInviteHandlerTest", function () {
|
|||
let mail = createTestEntity(MailTypeRef)
|
||||
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
|
||||
when(calendarModel.getCalendarInfos()).thenResolve(calendars)
|
||||
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.ACCEPTED, mail)).equals(ReplyResult.ReplySent)
|
||||
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.ACCEPTED, mail, mailboxDetails)).equals(
|
||||
ReplyResult.ReplySent,
|
||||
)
|
||||
o(calendarModel.processCalendarEventMessage.callCount).equals(1)
|
||||
})
|
||||
|
||||
|
@ -131,7 +137,9 @@ o.spec("CalendarInviteHandlerTest", function () {
|
|||
let mail = createTestEntity(MailTypeRef)
|
||||
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
|
||||
when(calendarModel.getCalendarInfos()).thenResolve(calendars)
|
||||
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail)).equals(ReplyResult.ReplySent)
|
||||
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail, mailboxDetails)).equals(
|
||||
ReplyResult.ReplySent,
|
||||
)
|
||||
o(calendarModel.processCalendarEventMessage.callCount).equals(0)
|
||||
})
|
||||
|
||||
|
@ -160,7 +168,9 @@ o.spec("CalendarInviteHandlerTest", function () {
|
|||
let mail = createTestEntity(MailTypeRef)
|
||||
mail.sender = createMailAddress({ address: sender, name: "whatever", contact: null })
|
||||
when(calendarModel.getCalendarInfos()).thenResolve(new Map())
|
||||
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail)).equals(ReplyResult.ReplySent)
|
||||
o(await calendarIniviteHandler.replyToEventInvitation(event, ownAttendee!, CalendarAttendeeStatus.DECLINED, mail, mailboxDetails)).equals(
|
||||
ReplyResult.ReplySent,
|
||||
)
|
||||
o(calendarModel.processCalendarEventMessage.callCount).equals(0)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -33,7 +33,7 @@ import { createTestEntity } from "../TestUtils.js"
|
|||
import { NoopProgressMonitor } from "../../../src/common/api/common/utils/ProgressMonitor.js"
|
||||
import { makeAlarmScheduler } from "./CalendarTestUtils.js"
|
||||
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { incrementByRepeatPeriod } from "../../../src/common/calendar/date/CalendarUtils.js"
|
||||
import { ExternalCalendarFacade } from "../../../src/common/native/common/generatedipc/ExternalCalendarFacade.js"
|
||||
import { DeviceConfig } from "../../../src/common/misc/DeviceConfig.js"
|
||||
|
@ -728,7 +728,7 @@ function makeLoginController(): LoginController {
|
|||
return loginController
|
||||
}
|
||||
|
||||
function makeMailModel(): MailModel {
|
||||
function makeMailModel(): MailboxModel {
|
||||
return downcast({})
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { Recipient, RecipientType } from "../../../src/common/api/common/recipie
|
|||
import { DateTime } from "luxon"
|
||||
import { createTestEntity } from "../TestUtils.js"
|
||||
import { matchers, object, when } from "testdouble"
|
||||
import { MailboxDetail } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { AlarmScheduler } from "../../../src/common/calendar/date/AlarmScheduler.js"
|
||||
|
||||
export const ownerMailAddress = "calendarowner@tutanota.de" as const
|
||||
|
@ -250,7 +250,6 @@ export function makeUserController(
|
|||
export function makeMailboxDetail(): MailboxDetail {
|
||||
return {
|
||||
mailbox: createTestEntity(MailBoxTypeRef),
|
||||
folders: new FolderSystem([]),
|
||||
mailGroupInfo: createTestEntity(GroupInfoTypeRef),
|
||||
mailGroup: createTestEntity(GroupTypeRef, {
|
||||
user: ownerId,
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from "../../../src/calendar-app/calendar/view/CalendarViewModel.js"
|
||||
import { CalendarInfo, CalendarModel } from "../../../src/calendar-app/calendar/model/CalendarModel.js"
|
||||
import { CalendarEventsRepository, DaysToEvents } from "../../../src/common/calendar/date/CalendarEventsRepository.js"
|
||||
import { MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { addDaysForEventInstance, getMonthRange } from "../../../src/common/calendar/date/CalendarUtils.js"
|
||||
import { CalendarEventModel, CalendarOperation, EventSaveResult } from "../../../src/calendar-app/calendar/gui/eventeditor-model/CalendarEventModel.js"
|
||||
|
||||
|
@ -72,7 +72,7 @@ o.spec("CalendarViewModel", function () {
|
|||
getUserController: () => userController,
|
||||
isInternalUserLoggedIn: () => true,
|
||||
})
|
||||
const mailModel: MailModel = object()
|
||||
const mailboxModel: MailboxModel = object()
|
||||
const previewModelFactory: CalendarEventPreviewModelFactory = async () => object()
|
||||
const viewModel = new CalendarViewModel(
|
||||
loginController,
|
||||
|
@ -86,7 +86,7 @@ o.spec("CalendarViewModel", function () {
|
|||
deviceConfig,
|
||||
calendarInvitations,
|
||||
zone,
|
||||
mailModel,
|
||||
mailboxModel,
|
||||
)
|
||||
viewModel.allowDrag = () => true
|
||||
return { viewModel, calendarModel, eventsRepository }
|
||||
|
|
|
@ -40,7 +40,7 @@ import { createTestEntity } from "../../TestUtils.js"
|
|||
import { areExcludedDatesEqual, areRepeatRulesEqual } from "../../../../src/common/calendar/date/CalendarUtils.js"
|
||||
import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js"
|
||||
import { FolderSystem } from "../../../../src/common/api/common/mail/FolderSystem.js"
|
||||
import { MailboxDetail } from "../../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail } from "../../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
|
||||
o.spec("CalendarEventModelTest", function () {
|
||||
let userController: UserController
|
||||
|
|
|
@ -6,24 +6,20 @@ import { MailSetKind, OperationType } from "../../../src/common/api/common/Tutan
|
|||
import { MailFolderTypeRef, MailTypeRef } from "../../../src/common/api/entities/tutanota/TypeRefs.js"
|
||||
import { EntityClient } from "../../../src/common/api/common/EntityClient.js"
|
||||
import { EntityRestClientMock } from "../api/worker/rest/EntityRestClientMock.js"
|
||||
import nodemocker from "../nodemocker.js"
|
||||
import { downcast } from "@tutao/tutanota-utils"
|
||||
import { MailFacade } from "../../../src/common/api/worker/facades/lazy/MailFacade.js"
|
||||
import { LoginController } from "../../../src/common/api/main/LoginController.js"
|
||||
import { matchers, object, when } from "testdouble"
|
||||
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
|
||||
import { WebsocketConnectivityModel } from "../../../src/common/misc/WebsocketConnectivityModel.js"
|
||||
import { UserController } from "../../../src/common/api/main/UserController.js"
|
||||
import { createTestEntity } from "../TestUtils.js"
|
||||
import { EntityUpdateData } from "../../../src/common/api/common/utils/EntityUpdateUtils.js"
|
||||
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { InboxRuleHandler } from "../../../src/mail-app/mail/model/InboxRuleHandler.js"
|
||||
import { getElementId, getListId } from "../../../src/common/api/common/utils/EntityUtils.js"
|
||||
|
||||
o.spec("MailModelTest", function () {
|
||||
let notifications: Partial<Notifications>
|
||||
let showSpy: Spy
|
||||
let model: MailModel
|
||||
let model: MailboxModel
|
||||
const inboxFolder = createTestEntity(MailFolderTypeRef, { _id: ["folderListId", "inboxId"], isMailSet: false })
|
||||
inboxFolder.mails = "instanceListId"
|
||||
inboxFolder.folderType = MailSetKind.INBOX
|
||||
|
@ -36,22 +32,15 @@ o.spec("MailModelTest", function () {
|
|||
const restClient: EntityRestClientMock = new EntityRestClientMock()
|
||||
|
||||
o.beforeEach(function () {
|
||||
mailboxDetails = [
|
||||
{
|
||||
folders: new FolderSystem([inboxFolder, anotherFolder]),
|
||||
},
|
||||
]
|
||||
notifications = {}
|
||||
showSpy = notifications.showNotification = spy()
|
||||
const connectivityModel = object<WebsocketConnectivityModel>()
|
||||
const mailFacade = nodemocker.mock<MailFacade>("mailFacade", {}).set()
|
||||
logins = object()
|
||||
let userController = object<UserController>()
|
||||
when(userController.isUpdateForLoggedInUserInstance(matchers.anything(), matchers.anything())).thenReturn(false)
|
||||
when(logins.getUserController()).thenReturn(userController)
|
||||
|
||||
inboxRuleHandler = object()
|
||||
model = new MailModel(downcast(notifications), downcast({}), mailFacade, new EntityClient(restClient), logins, connectivityModel, inboxRuleHandler)
|
||||
model = new MailboxModel(downcast({}), new EntityClient(restClient), logins)
|
||||
// not pretty, but works
|
||||
model.mailboxDetails(mailboxDetails as MailboxDetail[])
|
||||
})
|
|
@ -11,6 +11,7 @@ import {
|
|||
ConversationEntryTypeRef,
|
||||
createContact,
|
||||
CustomerAccountCreateDataTypeRef,
|
||||
Mail,
|
||||
MailAddressTypeRef,
|
||||
MailboxGroupRootTypeRef,
|
||||
MailboxPropertiesTypeRef,
|
||||
|
@ -45,7 +46,7 @@ import { NoZoneDateProvider } from "../../../src/common/api/common/utils/NoZoneD
|
|||
import { FolderSystem } from "../../../src/common/api/common/mail/FolderSystem.js"
|
||||
import { createTestEntity } from "../TestUtils.js"
|
||||
import { ContactModel } from "../../../src/common/contactsFunctionality/ContactModel.js"
|
||||
import { MailboxDetail, MailModel } from "../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { SendMailModel, TOO_MANY_VISIBLE_RECIPIENTS } from "../../../src/common/mailFunctionality/SendMailModel.js"
|
||||
import { RecipientField } from "../../../src/common/mailFunctionality/SharedMailUtils.js"
|
||||
import { getContactDisplayName } from "../../../src/common/contactsFunctionality/ContactUtils.js"
|
||||
|
@ -95,7 +96,7 @@ o.spec("SendMailModel", function () {
|
|||
lang.init(en)
|
||||
})
|
||||
|
||||
let mailModel: MailModel, entity: EntityClient, mailFacade: MailFacade, recipientsModel: RecipientsModel
|
||||
let mailboxModel: MailboxModel, entity: EntityClient, mailFacade: MailFacade, recipientsModel: RecipientsModel
|
||||
|
||||
let model: SendMailModel
|
||||
|
||||
|
@ -109,7 +110,7 @@ o.spec("SendMailModel", function () {
|
|||
).thenDo(() => ({ contacts: testIdGenerator.newId() }))
|
||||
when(entity.load(anything(), anything(), anything())).thenDo((typeRef, id, params) => ({ _type: typeRef, _id: id }))
|
||||
|
||||
mailModel = instance(MailModel)
|
||||
mailboxModel = instance(MailboxModel)
|
||||
|
||||
const contactModel = object<ContactModel>()
|
||||
when(contactModel.getContactListId()).thenResolve("contactListId")
|
||||
|
@ -153,7 +154,6 @@ o.spec("SendMailModel", function () {
|
|||
|
||||
const mailboxDetails: MailboxDetail = {
|
||||
mailbox: createTestEntity(MailBoxTypeRef),
|
||||
folders: new FolderSystem([]),
|
||||
mailGroupInfo: createTestEntity(GroupInfoTypeRef, {
|
||||
mailAddress: "mailgroup@addre.ss",
|
||||
}),
|
||||
|
@ -180,13 +180,16 @@ o.spec("SendMailModel", function () {
|
|||
mailFacade,
|
||||
entity,
|
||||
loginController,
|
||||
mailModel,
|
||||
mailboxModel,
|
||||
contactModel,
|
||||
eventController,
|
||||
mailboxDetails,
|
||||
recipientsModel,
|
||||
new NoZoneDateProvider(),
|
||||
mailboxProperties,
|
||||
async (mail: Mail) => {
|
||||
return false
|
||||
},
|
||||
)
|
||||
|
||||
replace(model, "getDefaultSender", () => DEFAULT_SENDER_FOR_TESTING)
|
||||
|
|
|
@ -19,7 +19,8 @@ import { matchers, object, when } from "testdouble"
|
|||
import { MailSetKind, MailState, OperationType } from "../../../../src/common/api/common/TutanotaConstants.js"
|
||||
import { isSameId } from "../../../../src/common/api/common/utils/EntityUtils.js"
|
||||
import { createTestEntity } from "../../TestUtils.js"
|
||||
import { MailboxDetail, MailModel } from "../../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { MailModel } from "../../../../src/mail-app/mail/model/MailModel.js"
|
||||
|
||||
o.spec("ConversationViewModel", function () {
|
||||
let conversation: ConversationEntry[]
|
||||
|
@ -29,6 +30,7 @@ o.spec("ConversationViewModel", function () {
|
|||
|
||||
let viewModel: ConversationViewModel
|
||||
let mailModel: MailModel
|
||||
let mailboxModel: MailboxModel
|
||||
let mailboxDetail: MailboxDetail
|
||||
let entityRestClientMock: EntityRestClientMock
|
||||
let prefProvider: ConversationPrefProvider
|
||||
|
|
|
@ -23,10 +23,10 @@ import { MailState } from "../../../../src/common/api/common/TutanotaConstants.j
|
|||
import { GroupInfoTypeRef } from "../../../../src/common/api/entities/sys/TypeRefs.js"
|
||||
import { CryptoFacade } from "../../../../src/common/api/worker/crypto/CryptoFacade.js"
|
||||
import { ContactImporter } from "../../../../src/mail-app/contacts/ContactImporter.js"
|
||||
import { MailboxDetail, MailModel } from "../../../../src/common/mailFunctionality/MailModel.js"
|
||||
import { MailboxDetail, MailboxModel } from "../../../../src/common/mailFunctionality/MailboxModel.js"
|
||||
import { ContactModel } from "../../../../src/common/contactsFunctionality/ContactModel.js"
|
||||
import { SendMailModel } from "../../../../src/common/mailFunctionality/SendMailModel.js"
|
||||
import { CalendarModel } from "../../../../src/calendar-app/calendar/model/CalendarModel.js"
|
||||
import { MailModel } from "../../../../src/mail-app/mail/model/MailModel.js"
|
||||
|
||||
o.spec("MailViewerViewModel", function () {
|
||||
let mail: Mail
|
||||
|
@ -34,6 +34,7 @@ o.spec("MailViewerViewModel", function () {
|
|||
let entityClient: EntityClient
|
||||
|
||||
let mailModel: MailModel
|
||||
let mailboxModel: MailboxModel
|
||||
let contactModel: ContactModel
|
||||
let configFacade: ConfigurationDatabase
|
||||
let fileController: FileController
|
||||
|
@ -46,11 +47,11 @@ o.spec("MailViewerViewModel", function () {
|
|||
let sendMailModelFactory: (mailboxDetails: MailboxDetail) => Promise<SendMailModel> = () => Promise.resolve(sendMailModel)
|
||||
let cryptoFacade: CryptoFacade
|
||||
let contactImporter: ContactImporter
|
||||
let calendarModel: CalendarModel
|
||||
|
||||
function makeViewModelWithHeaders(headers: string) {
|
||||
entityClient = object()
|
||||
mailModel = object()
|
||||
mailboxModel = object()
|
||||
contactModel = object()
|
||||
configFacade = object()
|
||||
fileController = object()
|
||||
|
@ -62,13 +63,13 @@ o.spec("MailViewerViewModel", function () {
|
|||
mailFacade = object()
|
||||
cryptoFacade = object()
|
||||
contactImporter = object()
|
||||
calendarModel = object()
|
||||
mail = prepareMailWithHeaders(mailFacade, headers)
|
||||
|
||||
return new MailViewerViewModel(
|
||||
mail,
|
||||
showFolder,
|
||||
entityClient,
|
||||
mailboxModel,
|
||||
mailModel,
|
||||
contactModel,
|
||||
configFacade,
|
||||
|
@ -81,7 +82,6 @@ o.spec("MailViewerViewModel", function () {
|
|||
mailFacade,
|
||||
cryptoFacade,
|
||||
async () => contactImporter,
|
||||
async () => calendarModel,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue