diff --git a/other/mail/taskdone/html.ejs b/other/mail/taskdone/html.ejs new file mode 100644 index 0000000..56a3d57 --- /dev/null +++ b/other/mail/taskdone/html.ejs @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+
TimeLimit
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+

+ <%= child %> hat angegeben, dass die Aufgabe + <%= task %> erledigt wurde. Die angeblich erledigten Aufgaben werden auf der Startseite der TimeLimit-App angezeigt. Dort kann die Erledigung bestätigt werden.

+

Da die Anzahl der Benachrichtigungs-E-Mails begrenzt ist erhalten Sie möglicherweise nicht bei jeder Aufgabenerledigung eine derartige E-Mail.

+

+ <%= child %> reported that + <%= task %> was finished. You can see the tasks which are marked as finished at the overview screen of the TimeLimit application. You can confirm the task completion there.

+

There are limits for the notifications mails so that it can happen that you do not get a mail for each completed task.

+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+

+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+

Sie erhalten diese Nachricht, weil Sie E-Mail-Benachrichtigungen in TimeLimit aktiviert haben. Sie können die Nachrichten in der TimeLimit-App abbestellen. Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten.

+

You got this mail because you enable mail notifications for this in TimeLimit. You can disable getting these messages in the TimeLimit App. If you have got any questions, then you can reply to this messagge.

+

© + <%= mailimprint %> +

+
+
+
+ +
+
+ +
+ + + diff --git a/other/mail/taskdone/htmltemplate-src.txt b/other/mail/taskdone/htmltemplate-src.txt new file mode 100644 index 0000000..649b339 --- /dev/null +++ b/other/mail/taskdone/htmltemplate-src.txt @@ -0,0 +1,57 @@ + + + + + TimeLimit + + + + + +

+ <%= child %> hat angegeben, dass die Aufgabe <%= task %> erledigt wurde. + Die angeblich erledigten Aufgaben werden auf der Startseite der TimeLimit-App + angezeigt. Dort kann die Erledigung bestätigt werden. +

+

+ Da die Anzahl der Benachrichtigungs-E-Mails begrenzt ist erhalten Sie + möglicherweise nicht bei jeder Aufgabenerledigung eine derartige E-Mail. +

+

+ <%= child %> reported that <%= task %> was finished. + You can see the tasks which are marked as finished at the overview screen + of the TimeLimit application. You can confirm the task completion there. +

+

+ There are limits for the notifications mails so that it can happen that + you do not get a mail for each completed task. +

+
+
+
+ + + + + + + + +

+ Sie erhalten diese Nachricht, weil Sie E-Mail-Benachrichtigungen in TimeLimit aktiviert haben. + Sie können die Nachrichten in der TimeLimit-App abbestellen. + Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. +

+

+ You got this mail because you enable mail notifications for this in TimeLimit. + You can disable getting these messages in the TimeLimit App. + If you have got any questions, then you can reply to this messagge. +

+

+ © <%= mailimprint %> +

+
+
+
+
+
diff --git a/other/mail/taskdone/subject.ejs b/other/mail/taskdone/subject.ejs new file mode 100644 index 0000000..358d176 --- /dev/null +++ b/other/mail/taskdone/subject.ejs @@ -0,0 +1 @@ +<%= child %>/<%= task %> diff --git a/other/mail/taskdone/text.ejs b/other/mail/taskdone/text.ejs new file mode 100644 index 0000000..67c1f76 --- /dev/null +++ b/other/mail/taskdone/text.ejs @@ -0,0 +1,25 @@ +<%= child %> hat angegeben, dass die Aufgabe <%= task %> erledigt wurde. +Die angeblich erledigten Aufgaben werden auf der Startseite der TimeLimit-App +angezeigt. Dort kann die Erledigung bestätigt werden. + +Da die Anzahl der Benachrichtigungs-E-Mails begrenzt ist erhalten Sie +möglicherweise nicht bei jeder Aufgabenerledigung eine derartige E-Mail. + +<%= child %> reported that <%= task %> was finished. +You can see the tasks which are marked as finished at the overview screen +of the TimeLimit application. You can confirm the task completion there. + +There are limits for the notifications mails so that it can happen that +you do not get a mail for each completed task. + +---------------------- + +Sie erhalten diese Nachricht, weil Sie E-Mail-Benachrichtigungen in TimeLimit aktiviert haben. +Sie können die Nachrichten in der TimeLimit-App abbestellen. +Falls Sie Fragen haben können Sie einfach auf diese E-Mail antworten. + +You got this mail because you enable mail notifications for this in TimeLimit. +You can disable getting these messages in the TimeLimit App. +If you have got any questions, then you can reply to this messagge. + + <%= mailimprint %> diff --git a/src/action/updateparentnotificationflags.ts b/src/action/updateparentnotificationflags.ts index 329ae05..6285781 100644 --- a/src/action/updateparentnotificationflags.ts +++ b/src/action/updateparentnotificationflags.ts @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +import { maxMailNotificationFlags } from '../database/user' import { ParentAction } from './basetypes' import { assertIdWithinFamily, assertSafeInteger, throwOutOfRange } from './meta/util' @@ -36,7 +37,7 @@ export class UpdateParentNotificationFlagsAction extends ParentAction { assertSafeInteger({ actionType, field: 'flags', value: flags }) - if (flags < 0 || flags > 1) { + if (flags < 0 || flags > maxMailNotificationFlags) { throwOutOfRange({ actionType, field: 'flags', value: flags }) } diff --git a/src/database/user.ts b/src/database/user.ts index b875492..fa22b67 100644 --- a/src/database/user.ts +++ b/src/database/user.ts @@ -22,6 +22,13 @@ import { optionalPasswordRegex, optionalSaltRegex } from '../util/password' import { booleanColumn, createEnumColumn, familyIdColumn, idWithinFamilyColumn, labelColumn, optionalIdWithinFamilyColumn, timestampColumn } from './columns' import { SequelizeAttributes } from './types' +export const maxMailNotificationFlags = 1 | 2 + +export const mailNotificationFlags = { + warnings: 1, + tasks: 2 +} + export interface UserAttributesVersion1 { familyId: string userId: string @@ -129,7 +136,7 @@ export const attributesVersion4: SequelizeAttributes = { defaultValue: 0, validate: { min: 0, - max: 1 + max: maxMailNotificationFlags } } } diff --git a/src/function/parent/create-family.ts b/src/function/parent/create-family.ts index 9d763ff..360fe35 100644 --- a/src/function/parent/create-family.ts +++ b/src/function/parent/create-family.ts @@ -18,6 +18,7 @@ import { Conflict } from 'http-errors' import { NewDeviceInfo, ParentPassword } from '../../api/schema' import { Database } from '../../database' +import { maxMailNotificationFlags } from '../../database/user' import { generateAuthToken, generateFamilyId, generateIdWithinFamily, generateVersionId } from '../../util/token' @@ -82,7 +83,7 @@ export const createFamily = async ({ database, mailAuthToken, firstParentDevice, currentDevice: '', categoryForNotAssignedApps: '', relaxPrimaryDeviceRule: false, - mailNotificationFlags: 1, // enable warning notifications + mailNotificationFlags: maxMailNotificationFlags, blockedTimes: '', flags: '0' }, { transaction }) diff --git a/src/function/sync/apply-actions/dispatch-app-logic-action/marktaskpendingaction.ts b/src/function/sync/apply-actions/dispatch-app-logic-action/marktaskpendingaction.ts index cd6e334..f60a268 100644 --- a/src/function/sync/apply-actions/dispatch-app-logic-action/marktaskpendingaction.ts +++ b/src/function/sync/apply-actions/dispatch-app-logic-action/marktaskpendingaction.ts @@ -16,8 +16,9 @@ */ import { MarkTaskPendingAction } from '../../../../action' +import { sendTaskDoneMails } from '../../../warningmail/taskdone' import { Cache } from '../cache' -import { IllegalStateException, SourceDeviceNotFoundException } from '../exception/illegal-state' +import { IllegalStateException, SourceDeviceNotFoundException, SourceUserNotFoundException } from '../exception/illegal-state' import { MissingTaskException } from '../exception/missing-item' export async function dispatchMarkTaskPendingAction ({ action, cache, deviceId }: { @@ -31,14 +32,15 @@ export async function dispatchMarkTaskPendingAction ({ action, cache, deviceId } taskId: action.taskId }, transaction: cache.transaction, - attributes: ['categoryId', 'pendingRequest'] + attributes: ['categoryId', 'pendingRequest', 'taskTitle'] }) if (taskInfoUnsafe === null) throw new MissingTaskException() const taskInfo = { categoryId: taskInfoUnsafe.categoryId, - pendingRequest: taskInfoUnsafe.pendingRequest + pendingRequest: taskInfoUnsafe.pendingRequest, + taskTitle: taskInfoUnsafe.taskTitle } if (taskInfo.pendingRequest !== 0) return // review already requested @@ -75,6 +77,19 @@ export async function dispatchMarkTaskPendingAction ({ action, cache, deviceId } throw new IllegalStateException({ staticMessage: 'Can not mark task pending for other user than the current user' }) } + const childInfoUnsafe = await cache.database.user.findOne({ + where: { + familyId: cache.familyId, + userId: categoryInfo.childId + }, + attributes: ['name'], + transaction: cache.transaction + }) + + if (childInfoUnsafe === null) throw new SourceUserNotFoundException() + + const childInfo = { name: childInfoUnsafe.name } + await cache.database.childTask.update({ pendingRequest: true }, { where: { familyId: cache.familyId, @@ -84,4 +99,12 @@ export async function dispatchMarkTaskPendingAction ({ action, cache, deviceId } }) cache.categoriesWithModifiedTasks.add(taskInfo.categoryId) + + await sendTaskDoneMails({ + database: cache.database, + transaction: cache.transaction, + familyId: cache.familyId, + childName: childInfo.name, + taskTitle: taskInfo.taskTitle + }) } diff --git a/src/function/warningmail/manipulation.ts b/src/function/warningmail/manipulation.ts index 7735ea9..26882cf 100644 --- a/src/function/warningmail/manipulation.ts +++ b/src/function/warningmail/manipulation.ts @@ -16,6 +16,7 @@ */ import { Database, Transaction, warpPromiseReturner } from '../../database' +import { mailNotificationFlags } from '../../database/user' import { sendManipulationWarningMail } from '../../util/mail' import { canSendWarningMail } from '../../util/ratelimit-warningmail' @@ -35,7 +36,7 @@ export const sendManipulationWarnings = async ({ database, familyId, deviceName, const targetMailAddresses = parentEntries .filter((item) => item.mail !== '') - .filter((item) => (item.mailNotificationFlags & 1) === 1) + .filter((item) => (item.mailNotificationFlags & mailNotificationFlags.warnings) === mailNotificationFlags.warnings) .map((item) => item.mail) transaction.afterCommit(warpPromiseReturner(async () => { diff --git a/src/function/warningmail/taskdone.ts b/src/function/warningmail/taskdone.ts new file mode 100644 index 0000000..48b39b2 --- /dev/null +++ b/src/function/warningmail/taskdone.ts @@ -0,0 +1,52 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Database, Transaction, warpPromiseReturner } from '../../database' +import { mailNotificationFlags } from '../../database/user' +import { sendTaskDoneMail } from '../../util/mail' +import { canSendTaskDoneMail } from '../../util/ratelimit-taskdonemail' + +export const sendTaskDoneMails = async ({ database, familyId, childName, taskTitle, transaction }: { + database: Database + familyId: string + childName: string + taskTitle: string + transaction: Transaction +}) => { + const parentEntries = await database.user.findAll({ + where: { + familyId, + type: 'parent' + }, + transaction + }) + + const targetMailAddresses = parentEntries + .filter((item) => item.mail !== '') + .filter((item) => (item.mailNotificationFlags & mailNotificationFlags.tasks) === mailNotificationFlags.tasks) + .map((item) => item.mail) + + transaction.afterCommit(warpPromiseReturner(async () => { + await Promise.all( + targetMailAddresses.map(async (receiver) => { + if (await canSendTaskDoneMail(receiver)) { + await sendTaskDoneMail({ receiver, child: childName, task: taskTitle }) + } + }) + ) + })) +} diff --git a/src/function/warningmail/uninstall.ts b/src/function/warningmail/uninstall.ts index efd1545..6cbec40 100644 --- a/src/function/warningmail/uninstall.ts +++ b/src/function/warningmail/uninstall.ts @@ -16,6 +16,7 @@ */ import { Database, Transaction, warpPromiseReturner } from '../../database' +import { mailNotificationFlags } from '../../database/user' import { sendUninstallWarningMail } from '../../util/mail' import { canSendWarningMail } from '../../util/ratelimit-warningmail' @@ -35,7 +36,7 @@ export const sendUninstallWarnings = async ({ database, familyId, deviceName, tr const targetMailAddresses = parentEntries .filter((item) => item.mail !== '') - .filter((item) => (item.mailNotificationFlags & 1) === 1) + .filter((item) => (item.mailNotificationFlags & mailNotificationFlags.warnings) === mailNotificationFlags.warnings) .map((item) => item.mail) transaction.afterCommit(warpPromiseReturner(async () => { diff --git a/src/util/mail.ts b/src/util/mail.ts index e38404f..6aa66f2 100644 --- a/src/util/mail.ts +++ b/src/util/mail.ts @@ -86,6 +86,20 @@ export const sendUninstallWarningMail = async ({ receiver, deviceName }: { }) } +export const sendTaskDoneMail = async ({ receiver, child, task }: { + receiver: string + child: string + task: string +}) => { + await email.send({ + template: join(__dirname, '../../other/mail/taskdone'), + message: { + to: receiver + }, + locals: { child, task, mailimprint } + }) +} + export function isMailServerBlacklisted (mail: string): boolean { const parts = mail.split('@') const domain = parts[parts.length - 1] diff --git a/src/util/ratelimit-taskdonemail.ts b/src/util/ratelimit-taskdonemail.ts new file mode 100644 index 0000000..350b177 --- /dev/null +++ b/src/util/ratelimit-taskdonemail.ts @@ -0,0 +1,63 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2020 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { RateLimiterAbstract, RateLimiterMemory } from 'rate-limiter-flexible' + +const individualMailLimitMinute: RateLimiterAbstract = new RateLimiterMemory({ + keyPrefix: 'timelimit:sendmail-taskdone:individual:minute', + points: 1, + duration: 60 // 1 minute +}) + +const individualMailLimitFiveMinutes: RateLimiterAbstract = new RateLimiterMemory({ + keyPrefix: 'timelimit:sendmail-taskdone:individual:fiveminutes', + points: 3, + duration: 60 * 5 // 5 minutes +}) + +const individualMailLimitHourly: RateLimiterAbstract = new RateLimiterMemory({ + keyPrefix: 'timelimit:sendmail-taskdone:individual:hourly', + points: 5, + duration: 60 * 60 // 1 hour +}) + +const individualMailLimitDay: RateLimiterAbstract = new RateLimiterMemory({ + keyPrefix: 'timelimit:sendmail-taskdone:individual:day', + points: 10, + duration: 60 * 60 * 24 // 1 day +}) + +const checkIndividualMailSendLimit = async (receiver: string) => { + await individualMailLimitMinute.consume(receiver) + await individualMailLimitFiveMinutes.consume(receiver) + await individualMailLimitHourly.consume(receiver) + await individualMailLimitDay.consume(receiver) +} + +const checkMailSendLimit = async (receiver: string) => { + await checkIndividualMailSendLimit(receiver) +} + +export const canSendTaskDoneMail = async (receiver: string) => { + try { + await checkMailSendLimit(receiver) + + return true + } catch (ex) { + return false + } +}