Add task completion mails

This commit is contained in:
Jonas Lochmann
2020-12-21 01:00:00 +01:00
parent 2acee613f6
commit 13ede06a91
13 changed files with 541 additions and 8 deletions
+287
View File
@@ -0,0 +1,287 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title> </title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style type="text/css">
</style>
</head>
<body>
<div style="">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#009688;background-color:#009688;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#009688;background-color:#009688;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:20px;line-height:1;text-align:left;color:#ffffff;">TimeLimit</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p>
<%= 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. </p>
<p> Da die Anzahl der Benachrichtigungs-E-Mails begrenzt ist erhalten Sie möglicherweise nicht bei jeder Aufgabenerledigung eine derartige E-Mail. </p>
<p>
<%= 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. </p>
<p> There are limits for the notifications mails so that it can happen that you do not get a mail for each completed task. </p>
</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:dashed 1px lightgrey;font-size:1px;margin:0px auto;width:100%;"> </p>
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:dashed 1px lightgrey;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px"
>
<tr>
<td style="height:0;line-height:0;">
&nbsp;
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;">
<p> 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. </p>
<p> 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. </p>
<p> &copy;
<%= mailimprint %>
</p>
</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<mjml>
<mj-body>
<mj-section background-color="#009688">
<mj-column>
<mj-text font-size="20px" color="#ffffff">TimeLimit</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text>
<p>
<%= 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.
</p>
<p>
Da die Anzahl der Benachrichtigungs-E-Mails begrenzt ist erhalten Sie
möglicherweise nicht bei jeder Aufgabenerledigung eine derartige E-Mail.
</p>
<p>
<%= 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.
</p>
<p>
There are limits for the notifications mails so that it can happen that
you do not get a mail for each completed task.
</p>
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-divider border-width="1px" border-style="dashed" border-color="lightgrey" />
</mj-column>
</mj-section>
<mj-section>
<mj-column>
<mj-text>
<p>
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.
</p>
<p>
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.
</p>
<p>
&copy; <%= mailimprint %>
</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+1
View File
@@ -0,0 +1 @@
<%= child %>/<%= task %>
+25
View File
@@ -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.
<C> <%= mailimprint %>
+2 -1
View File
@@ -15,6 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 })
}
+8 -1
View File
@@ -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<UserAttributesVersion4> = {
defaultValue: 0,
validate: {
min: 0,
max: 1
max: maxMailNotificationFlags
}
}
}
+2 -1
View File
@@ -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 })
@@ -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
})
}
+2 -1
View File
@@ -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 () => {
+52
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 })
}
})
)
}))
}
+2 -1
View File
@@ -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 () => {
+14
View File
@@ -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]
+63
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}