@social-mail/social-mail-web-server 1.7.261 → 1.7.263
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/model/entities/DomainReputation.d.ts +6 -0
- package/dist/server/model/entities/DomainReputation.d.ts.map +1 -0
- package/dist/server/model/entities/DomainReputation.js +30 -0
- package/dist/server/model/entities/DomainReputation.js.map +1 -0
- package/dist/server/seed/ui/seed-ui.js +1 -1
- package/dist/server/services/EmailAddressService.d.ts +1 -0
- package/dist/server/services/EmailAddressService.d.ts.map +1 -1
- package/dist/server/services/EmailAddressService.js +42 -18
- package/dist/server/services/EmailAddressService.js.map +1 -1
- package/dist/server/services/message-events/MessagingService.d.ts +1 -0
- package/dist/server/services/message-events/MessagingService.d.ts.map +1 -1
- package/dist/server/services/message-events/MessagingService.js +1 -0
- package/dist/server/services/message-events/MessagingService.js.map +1 -1
- package/dist/server/smtp/services/ChannelService.d.ts +5 -1
- package/dist/server/smtp/services/ChannelService.d.ts.map +1 -1
- package/dist/server/smtp/services/ChannelService.js +11 -7
- package/dist/server/smtp/services/ChannelService.js.map +1 -1
- package/dist/server/smtp/services/DomainReputationService.d.ts +10 -0
- package/dist/server/smtp/services/DomainReputationService.d.ts.map +1 -0
- package/dist/server/smtp/services/DomainReputationService.js +108 -0
- package/dist/server/smtp/services/DomainReputationService.js.map +1 -0
- package/dist/server/smtp/services/MailReceiverService.d.ts +4 -2
- package/dist/server/smtp/services/MailReceiverService.d.ts.map +1 -1
- package/dist/server/smtp/services/MailReceiverService.js +31 -17
- package/dist/server/smtp/services/MailReceiverService.js.map +1 -1
- package/dist/server/workflows/daily/DailyWorkflow.d.ts +2 -1
- package/dist/server/workflows/daily/DailyWorkflow.d.ts.map +1 -1
- package/dist/server/workflows/daily/DailyWorkflow.js +6 -2
- package/dist/server/workflows/daily/DailyWorkflow.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/server/model/entities/DomainReputation.ts +15 -0
- package/src/server/seed/ui/seed-ui.ts +1 -1
- package/src/server/services/EmailAddressService.ts +10 -0
- package/src/server/services/message-events/MessagingService.ts +3 -0
- package/src/server/smtp/services/ChannelService.ts +13 -7
- package/src/server/smtp/services/DomainReputationService.ts +44 -0
- package/src/server/smtp/services/MailReceiverService.ts +35 -21
- package/src/server/workflows/daily/DailyWorkflow.ts +5 -1
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Column from "@entity-access/entity-access/dist/decorators/Column.js";
|
|
2
|
+
import Table from "@entity-access/entity-access/dist/decorators/Table.js";
|
|
3
|
+
|
|
4
|
+
@Table("DomainReputations")
|
|
5
|
+
export default class DomainReputation {
|
|
6
|
+
|
|
7
|
+
@Column({ dataType: "Char", length: 400, key: true })
|
|
8
|
+
public name: string;
|
|
9
|
+
|
|
10
|
+
@Column({ dataType: "Int", default: () => 0})
|
|
11
|
+
public received: number;
|
|
12
|
+
|
|
13
|
+
@Column({ dataType: "Int", default: () => 0})
|
|
14
|
+
public failed: number;
|
|
15
|
+
}
|
|
@@ -116,6 +116,16 @@ export default class EmailAddressService implements ICacheContainer<EmailAddress
|
|
|
116
116
|
.first();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
@cacheFor()
|
|
120
|
+
public async getHostedMailboxByAddress(address: string) {
|
|
121
|
+
address = address.trim().toLowerCase();
|
|
122
|
+
using scope = globalServices.createScope();
|
|
123
|
+
const db = scope.resolve(SocialMailContext);
|
|
124
|
+
return await db.mailboxes.where({ address }, (p) => (x) => x.emailAddress.emailAddress === p.address)
|
|
125
|
+
.include((x) => x.emailAddress)
|
|
126
|
+
.first();
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
@cacheFor()
|
|
120
130
|
public async isHostedChannelID(emailAddressID) {
|
|
121
131
|
using scope = globalServices.createScope();
|
|
@@ -17,6 +17,8 @@ export default class MessagingService extends ServiceObject {
|
|
|
17
17
|
|
|
18
18
|
refreshDns: IMessageChannel<{ serverID }>;
|
|
19
19
|
|
|
20
|
+
reputationCleared: IMessageChannel;
|
|
21
|
+
|
|
20
22
|
@Inject
|
|
21
23
|
private pgEvents: PgEvents;
|
|
22
24
|
|
|
@@ -24,6 +26,7 @@ export default class MessagingService extends ServiceObject {
|
|
|
24
26
|
this.channelRefresh = this.segment(`web-site-channel-host-clear-${globalEnv.serverID}`);
|
|
25
27
|
this.refreshDns = this.segment("refresh-dns");
|
|
26
28
|
this.snapshotRefresh = this.segment(`snapshot-refresh-${globalEnv.serverID}`);
|
|
29
|
+
this.reputationCleared = this.segment(`reputation-cleared-${globalEnv.serverID}`);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
notify(messageName: string, data: any): any {
|
|
@@ -17,21 +17,27 @@ export default class ChannelService {
|
|
|
17
17
|
@Inject
|
|
18
18
|
private db: SocialMailContext;
|
|
19
19
|
|
|
20
|
-
async getAccess(senderID: number,
|
|
20
|
+
async getAccess({ senderID, senderAddress, recipientChannel }: { senderAddress?: string, senderID?: number, recipientChannel: Mailbox }) {
|
|
21
21
|
|
|
22
|
-
if (!
|
|
22
|
+
if (!recipientChannel.isPrivateChannel && !recipientChannel.requiresMembershipApproval) {
|
|
23
23
|
// we should accept everything...
|
|
24
|
-
return { channel };
|
|
24
|
+
return { channel: recipientChannel };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const { mailboxID } =
|
|
27
|
+
const { mailboxID } = recipientChannel;
|
|
28
|
+
|
|
29
|
+
let q = this.db.mailboxContacts.where({ mailboxID }, (p) => (x) => x.mailboxID === p.mailboxID);
|
|
30
|
+
|
|
31
|
+
q = senderAddress
|
|
32
|
+
? q.where({ senderAddress}, (p) => (x) => x.emailAddress.emailAddress === p.senderAddress)
|
|
33
|
+
: q.where({ senderID}, (p) => (x) => x.emailAddressID === p.senderID);
|
|
28
34
|
|
|
29
35
|
const mailboxContact = await this.db.mailboxContacts
|
|
30
36
|
.where({ senderID, mailboxID}, (p) => (x) => x.mailboxID === p.mailboxID
|
|
31
37
|
&& x.emailAddressID === p.senderID)
|
|
32
38
|
.first();
|
|
33
39
|
|
|
34
|
-
if (
|
|
40
|
+
if (recipientChannel.isPrivateChannel) {
|
|
35
41
|
if (!mailboxContact) {
|
|
36
42
|
throw new SmtpError("Only active members of channel can send message to channel", SmtpStatusCodes.DeliveryNotPermitted);
|
|
37
43
|
}
|
|
@@ -40,7 +46,7 @@ export default class ChannelService {
|
|
|
40
46
|
if (mailboxContact?.blocked) {
|
|
41
47
|
throw new SmtpError("Only active members of channel can send message to channel", SmtpStatusCodes.DeliveryNotPermitted);
|
|
42
48
|
}
|
|
43
|
-
return { channel, mailboxContact };
|
|
49
|
+
return { channel: recipientChannel, mailboxContact };
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
async subscribe(mailboxID: number, emailAddressID: number) {
|
|
@@ -73,7 +79,7 @@ export default class ChannelService {
|
|
|
73
79
|
const { senderID } = entity.email;
|
|
74
80
|
const channel = await this.db.mailboxes.where({ emailAddressID },(p) => (x) => x.mailboxID === p.emailAddressID)
|
|
75
81
|
.first();
|
|
76
|
-
const { mailboxContact } = await this.getAccess(senderID, channel );
|
|
82
|
+
const { mailboxContact } = await this.getAccess({ senderID, recipientChannel: channel } );
|
|
77
83
|
const isActiveSubscriber = mailboxContact && (mailboxContact.isOwner || mailboxContact.canContribute || mailboxContact.status === "active");
|
|
78
84
|
|
|
79
85
|
if(isActiveSubscriber) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import Inject, { RegisterSingleton, ServiceObject, ServiceProvider } from "@entity-access/entity-access/dist/di/di.js";
|
|
2
|
+
import { cacheFor, ICacheContainer, ObjectCache } from "../../../common/cacheFor.js";
|
|
3
|
+
import { SmtpStatusCodes } from "../client/SmtpStatusCodes.js";
|
|
4
|
+
import SmtpError from "./SmtpError.js";
|
|
5
|
+
import SocialMailContext from "../../model/SocialMailContext.js";
|
|
6
|
+
import MessagingService from "../../services/message-events/MessagingService.js";
|
|
7
|
+
|
|
8
|
+
@RegisterSingleton
|
|
9
|
+
export default class DomainReputationService
|
|
10
|
+
extends ServiceObject
|
|
11
|
+
implements ICacheContainer<DomainReputationService> {
|
|
12
|
+
|
|
13
|
+
cache: ObjectCache<DomainReputationService>;
|
|
14
|
+
|
|
15
|
+
@Inject
|
|
16
|
+
ms: MessagingService;
|
|
17
|
+
|
|
18
|
+
@cacheFor()
|
|
19
|
+
async checkReputation(domainName: string) {
|
|
20
|
+
using scope = ServiceProvider.createScope(this);
|
|
21
|
+
const db = scope.resolve(SocialMailContext);
|
|
22
|
+
|
|
23
|
+
domainName = domainName.trim().toLowerCase();
|
|
24
|
+
|
|
25
|
+
const reputation = await db.domains.where({ domainName }, (p) => (x) => x.name === p.domainName)
|
|
26
|
+
.first();
|
|
27
|
+
|
|
28
|
+
if (!reputation) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { failed } = reputation;
|
|
33
|
+
|
|
34
|
+
if (failed > 3) {
|
|
35
|
+
if (failed > (reputation.received / 10)) {
|
|
36
|
+
throw new SmtpError("Sender Blocked for 24 Hours", SmtpStatusCodes.SenderBlocked);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
postInit() {
|
|
42
|
+
this.ms.reputationCleared.on(() => this.cache?.clear("checkReputation"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -25,6 +25,8 @@ import { serverActivities } from "../../model/entities/ServerActivity.js";
|
|
|
25
25
|
import AccessTokenService from "./AccessTokenService.js";
|
|
26
26
|
import EntityAccessError from "@entity-access/entity-access/dist/common/EntityAccessError.js";
|
|
27
27
|
import TagService from "../../services/tags/TagService.js";
|
|
28
|
+
import DomainReputationService from "./DomainReputationService.js";
|
|
29
|
+
import MessagingService from "../../services/message-events/MessagingService.js";
|
|
28
30
|
|
|
29
31
|
const stripBrackets = (text: string) => {
|
|
30
32
|
if (!text) {
|
|
@@ -97,11 +99,19 @@ export default class MailReceiverService {
|
|
|
97
99
|
@Inject
|
|
98
100
|
private tagService: TagService;
|
|
99
101
|
|
|
102
|
+
@Inject
|
|
103
|
+
private domainReputation: DomainReputationService;
|
|
104
|
+
|
|
105
|
+
@Inject
|
|
106
|
+
private ms: MessagingService;
|
|
107
|
+
|
|
100
108
|
private session: smtpServer.SMTPServerSession;
|
|
101
109
|
|
|
102
110
|
private from: smtpServer.SMTPServerAddress;
|
|
103
111
|
|
|
104
|
-
private
|
|
112
|
+
private fromDomain: string;
|
|
113
|
+
|
|
114
|
+
// private sender: EmailAddress;
|
|
105
115
|
|
|
106
116
|
private recipients: Map<string, Mailbox>;
|
|
107
117
|
|
|
@@ -129,20 +139,11 @@ export default class MailReceiverService {
|
|
|
129
139
|
|
|
130
140
|
this.from = address;
|
|
131
141
|
|
|
132
|
-
const
|
|
133
|
-
const { mailbox, domainID } = sender;
|
|
142
|
+
const domain = this.fromDomain = this.from.address.split("@",2).pop();
|
|
134
143
|
|
|
135
|
-
|
|
136
|
-
.first();
|
|
137
|
-
|
|
138
|
-
const { failed } = reputation;
|
|
139
|
-
|
|
140
|
-
if (failed > 3) {
|
|
141
|
-
if (failed > (reputation.received / 10)) {
|
|
142
|
-
throw new SmtpError("Sender Blocked for 24 Hours", SmtpStatusCodes.SenderBlocked);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
144
|
+
await this.domainReputation.checkReputation(domain);
|
|
145
145
|
|
|
146
|
+
const mailbox = await this.emailAddressService.getHostedMailboxByAddress(address.address);
|
|
146
147
|
if (mailbox && !mailbox.isExternal) {
|
|
147
148
|
if (!this.session.user) {
|
|
148
149
|
console.error(`Login required for ${address.address}`);
|
|
@@ -159,13 +160,11 @@ export default class MailReceiverService {
|
|
|
159
160
|
throw new SmtpError("Unknown sequence of commands", SmtpStatusCodes.BadCommandSequence);
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
const domainID = this.sender.domainID;
|
|
163
|
-
|
|
164
163
|
const mailbox = await this.db.mailboxes
|
|
165
164
|
.where(address, (p) => (x) => x.emailAddress.emailAddress === p.address)
|
|
166
165
|
.first();
|
|
167
166
|
if (mailbox == null) {
|
|
168
|
-
await this.addDomainActivity(
|
|
167
|
+
await this.addDomainActivity("rcpt-not-found");
|
|
169
168
|
|
|
170
169
|
// update domain reputation
|
|
171
170
|
throw new SmtpError(`Mailbox ${address.address} not found`, SmtpStatusCodes.MailboxUnavailable);
|
|
@@ -174,7 +173,7 @@ export default class MailReceiverService {
|
|
|
174
173
|
throw new SmtpError("Mailbox full", SmtpStatusCodes.InsufficientStorage);
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
await this.addDomainActivity(
|
|
176
|
+
await this.addDomainActivity("mail-received");
|
|
178
177
|
|
|
179
178
|
// check if email is blocked...
|
|
180
179
|
const mailboxID = mailbox.mailboxID;
|
|
@@ -200,26 +199,41 @@ export default class MailReceiverService {
|
|
|
200
199
|
|
|
201
200
|
if (mailbox.isChannel) {
|
|
202
201
|
const channelService = ServiceProvider.resolve(this, ChannelService);
|
|
203
|
-
await channelService.getAccess(
|
|
202
|
+
await channelService.getAccess({
|
|
203
|
+
senderAddress: address.address,
|
|
204
|
+
recipientChannel: mailbox
|
|
205
|
+
});
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
this.recipients.set(address.address, mailbox);
|
|
207
209
|
}
|
|
208
210
|
|
|
209
|
-
async addDomainActivity(
|
|
211
|
+
async addDomainActivity(activity: serverActivities) {
|
|
210
212
|
const ipAddress = this.session.remoteAddress;
|
|
213
|
+
|
|
214
|
+
const domainID = await this.emailAddressService.getOrCreateDomain(this.fromDomain);
|
|
215
|
+
|
|
211
216
|
await this.db.serverActivities.statements.insert({
|
|
212
217
|
domainID,
|
|
213
218
|
activity,
|
|
214
219
|
ipAddress
|
|
215
220
|
});
|
|
216
221
|
|
|
217
|
-
await this.db.domains
|
|
222
|
+
const [d] = await this.db.domains
|
|
218
223
|
.where({ domainID }, (p) => (x) => x.domainID === p.domainID)
|
|
219
|
-
.
|
|
224
|
+
.updateSelect(void 0, (p) => (x) => ({
|
|
220
225
|
received: Sql.coll.sum(x.activities.map((a) => a.activity === "mail-received" ? 1 : 0)),
|
|
221
226
|
failed: Sql.coll.sum(x.activities.map((a) => a.activity === "rcpt-not-found" ? 1 : 0)),
|
|
222
227
|
}));
|
|
228
|
+
|
|
229
|
+
if (activity === "rcpt-not-found") {
|
|
230
|
+
if (d?.failed > 3) {
|
|
231
|
+
if (d.failed > (d.received / 10)) {
|
|
232
|
+
// clear on reputation change
|
|
233
|
+
this.ms.reputationCleared.notify("");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
223
237
|
}
|
|
224
238
|
|
|
225
239
|
async acceptData(stream: smtpServer.SMTPServerDataStream): Promise<void> {
|
|
@@ -11,6 +11,7 @@ import IndexAppFilesWorkflow from "../search/IndexAppFilesWorkflow.js";
|
|
|
11
11
|
import DbLockService from "../../services/db-lock/DbLockService.js";
|
|
12
12
|
import PendingPostReceiveWorkflow from "../pending/PendingPostReceiveWorkflow.js";
|
|
13
13
|
import PendingReferencesWorkflow from "../pending/PendingReferencesWorkflow.js";
|
|
14
|
+
import MessagingService from "../../services/message-events/MessagingService.js";
|
|
14
15
|
|
|
15
16
|
export default class DailyWorkflow extends Workflow<any,any> {
|
|
16
17
|
|
|
@@ -42,7 +43,8 @@ export default class DailyWorkflow extends Workflow<any,any> {
|
|
|
42
43
|
async startSync(
|
|
43
44
|
index: number,
|
|
44
45
|
@Inject db?: SocialMailContext,
|
|
45
|
-
@Inject lockService?: DbLockService
|
|
46
|
+
@Inject lockService?: DbLockService,
|
|
47
|
+
@Inject ms?: MessagingService
|
|
46
48
|
) {
|
|
47
49
|
// fetch all sync items...
|
|
48
50
|
db.verifyFilters = false;
|
|
@@ -77,6 +79,8 @@ export default class DailyWorkflow extends Workflow<any,any> {
|
|
|
77
79
|
.limit(1000)
|
|
78
80
|
.delete(void 0, (p) => (x) => x.activityID !== null);
|
|
79
81
|
|
|
82
|
+
await ms.reputationCleared.notify("Reputation cleared");
|
|
83
|
+
|
|
80
84
|
// deactivate all expired sessions...
|
|
81
85
|
await db.loginSessions
|
|
82
86
|
.where(void 0, (p) => (x) => x.expiry <= (Sql.date.now()))
|