Skip to content

Commit 3bfbc2e

Browse files
committed
feat: add smtp counts to user admin page for 1 hour, 24 hours and 72 hours
1 parent fda0cf7 commit 3bfbc2e

File tree

2 files changed

+100
-6
lines changed

2 files changed

+100
-6
lines changed

app/controllers/web/admin/users.js

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const parser = require('mongodb-query-parser');
1212
const { boolean } = require('boolean');
1313
const _ = require('#helpers/lodash');
1414

15-
const { Users } = require('#models');
15+
const { Users, Emails } = require('#models');
1616
const config = require('#config');
1717

1818
const REGEX_BYTES = new RE2(/^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i);
@@ -52,29 +52,91 @@ async function list(ctx) {
5252
}
5353
}
5454

55-
const [users, itemCount] = await Promise.all([
55+
const now = new Date();
56+
const oneHourAgo = new Date(now - 60 * 60 * 1000);
57+
const twentyFourHoursAgo = new Date(now - 24 * 60 * 60 * 1000);
58+
const seventyTwoHoursAgo = new Date(now - 72 * 60 * 60 * 1000);
59+
60+
const [users, itemCount, emailCounts] = await Promise.all([
5661
// eslint-disable-next-line unicorn/no-array-callback-reference
5762
Users.find(query)
5863
.limit(ctx.query.limit)
5964
.skip(ctx.paginate.skip)
6065
.lean()
6166
.sort(ctx.query.sort || '-created_at')
6267
.exec(),
63-
Users.countDocuments(query)
68+
Users.countDocuments(query),
69+
// Get SMTP outbound email counts per user with time-based breakdowns
70+
Emails.aggregate([
71+
{
72+
$match: {
73+
// Only count delivered/sent emails (not failed/bounced)
74+
status: { $in: ['delivered', 'deferred', 'sent'] }
75+
}
76+
},
77+
{
78+
$group: {
79+
_id: '$user',
80+
totalEmails: { $sum: 1 },
81+
lastEmailAt: { $max: '$created_at' },
82+
// Count emails within time periods
83+
emailsLast1Hour: {
84+
$sum: {
85+
$cond: [{ $gte: ['$created_at', oneHourAgo] }, 1, 0]
86+
}
87+
},
88+
emailsLast24Hours: {
89+
$sum: {
90+
$cond: [{ $gte: ['$created_at', twentyFourHoursAgo] }, 1, 0]
91+
}
92+
},
93+
emailsLast72Hours: {
94+
$sum: {
95+
$cond: [{ $gte: ['$created_at', seventyTwoHoursAgo] }, 1, 0]
96+
}
97+
}
98+
}
99+
}
100+
])
64101
]);
65102

103+
// Create a map for quick lookup of email counts
104+
const emailCountMap = new Map();
105+
for (const count of emailCounts) {
106+
emailCountMap.set(count._id.toString(), {
107+
totalEmails: count.totalEmails,
108+
lastEmailAt: count.lastEmailAt,
109+
emailsLast1Hour: count.emailsLast1Hour,
110+
emailsLast24Hours: count.emailsLast24Hours,
111+
emailsLast72Hours: count.emailsLast72Hours
112+
});
113+
}
114+
115+
// Add email counts to each user
116+
const usersWithEmailCounts = users.map((user) => ({
117+
...user,
118+
totalEmails: emailCountMap.get(user._id.toString())?.totalEmails || 0,
119+
lastEmailAt: emailCountMap.get(user._id.toString())?.lastEmailAt || null,
120+
emailsLast1Hour:
121+
emailCountMap.get(user._id.toString())?.emailsLast1Hour || 0,
122+
emailsLast24Hours:
123+
emailCountMap.get(user._id.toString())?.emailsLast24Hours || 0,
124+
emailsLast72Hours:
125+
emailCountMap.get(user._id.toString())?.emailsLast72Hours || 0
126+
}));
127+
66128
const pageCount = Math.ceil(itemCount / ctx.query.limit);
67129

68130
if (ctx.accepts('html'))
69131
return ctx.render('admin/users', {
70-
users,
132+
users: usersWithEmailCounts,
71133
pageCount,
72134
itemCount,
73135
pages: paginate.getArrayPages(ctx)(6, pageCount, ctx.query.page)
74136
});
75137

76138
const table = await ctx.render('admin/users/_table', {
77-
users,
139+
users: usersWithEmailCounts,
78140
pageCount,
79141
itemCount,
80142
pages: paginate.getArrayPages(ctx)(6, pageCount, ctx.query.page)

app/views/admin/users/_table.pug

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ include ../../_pagination
2525
+sortHeader('max_quota_per_alias', 'Max Qouta', '#table-users')
2626
th(scope="col")
2727
+sortHeader('smtp_limit', 'SMTP Limit', '#table-users')
28+
th(scope="col")
29+
+sortHeader('totalEmails', 'Emails Sent', '#table-users')
30+
th(scope="col")
31+
+sortHeader('emailsLast1Hour', '1hr', '#table-users')
32+
th(scope="col")
33+
+sortHeader('emailsLast24Hours', '24hr', '#table-users')
34+
th(scope="col")
35+
+sortHeader('emailsLast72Hours', '72hr', '#table-users')
36+
th(scope="col")
37+
+sortHeader('lastEmailAt', 'Last Email', '#table-users')
2838
th(scope="col")
2939
+sortHeader('created_at', 'Created', '#table-users')
3040
th(scope="col")
@@ -37,7 +47,7 @@ include ../../_pagination
3747
th.text-center.align-middle(scope="col")= t("Actions")
3848
tbody
3949
if users.length === 0
40-
td.alert.alert-info(colspan=passport && passport.otp ? "14" : "13")= t("No users exist for that search.")
50+
td.alert.alert-info(colspan=passport && passport.otp ? "19" : "18")= t("No users exist for that search.")
4151
else
4252
each user in users
4353
tr
@@ -116,6 +126,28 @@ include ../../_pagination
116126
data-toggle="tooltip",
117127
data-title=t("Update")
118128
): i.fa.fa-fw.fa-save
129+
td.align-middle.text-right
130+
if user.totalEmails > 0
131+
a(href=l(`/admin/emails?user=${user._id}`), target="_blank")= user.totalEmails.toLocaleString()
132+
else
133+
= user.totalEmails
134+
td.align-middle.text-right
135+
span.badge(
136+
class=user.emailsLast1Hour > 0 ? "badge-warning" : "badge-light"
137+
)= user.emailsLast1Hour
138+
td.align-middle.text-right
139+
span.badge(
140+
class=user.emailsLast24Hours > 0 ? "badge-info" : "badge-light"
141+
)= user.emailsLast24Hours
142+
td.align-middle.text-right
143+
span.badge(
144+
class=user.emailsLast72Hours > 0 ? "badge-primary" : "badge-light"
145+
)= user.emailsLast72Hours
146+
td.align-middle
147+
if user.lastEmailAt
148+
.dayjs(data-time=new Date(user.lastEmailAt).getTime())= dayjs(user.lastEmailAt).tz(user.timezone === 'Etc/Unknown' ? 'UTC' : user.timezone).format("M/D/YY h:mm A z")
149+
else
150+
= "Never"
119151
td.align-middle.dayjs(
120152
data-time=new Date(user.created_at).getTime()
121153
)= dayjs(user.created_at).tz(user.timezone === 'Etc/Unknown' ? 'UTC' : user.timezone).format("M/D/YY h:mm A z")

0 commit comments

Comments
 (0)