@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.
Files changed (39) hide show
  1. package/dist/server/model/entities/DomainReputation.d.ts +6 -0
  2. package/dist/server/model/entities/DomainReputation.d.ts.map +1 -0
  3. package/dist/server/model/entities/DomainReputation.js +30 -0
  4. package/dist/server/model/entities/DomainReputation.js.map +1 -0
  5. package/dist/server/seed/ui/seed-ui.js +1 -1
  6. package/dist/server/services/EmailAddressService.d.ts +1 -0
  7. package/dist/server/services/EmailAddressService.d.ts.map +1 -1
  8. package/dist/server/services/EmailAddressService.js +42 -18
  9. package/dist/server/services/EmailAddressService.js.map +1 -1
  10. package/dist/server/services/message-events/MessagingService.d.ts +1 -0
  11. package/dist/server/services/message-events/MessagingService.d.ts.map +1 -1
  12. package/dist/server/services/message-events/MessagingService.js +1 -0
  13. package/dist/server/services/message-events/MessagingService.js.map +1 -1
  14. package/dist/server/smtp/services/ChannelService.d.ts +5 -1
  15. package/dist/server/smtp/services/ChannelService.d.ts.map +1 -1
  16. package/dist/server/smtp/services/ChannelService.js +11 -7
  17. package/dist/server/smtp/services/ChannelService.js.map +1 -1
  18. package/dist/server/smtp/services/DomainReputationService.d.ts +10 -0
  19. package/dist/server/smtp/services/DomainReputationService.d.ts.map +1 -0
  20. package/dist/server/smtp/services/DomainReputationService.js +108 -0
  21. package/dist/server/smtp/services/DomainReputationService.js.map +1 -0
  22. package/dist/server/smtp/services/MailReceiverService.d.ts +4 -2
  23. package/dist/server/smtp/services/MailReceiverService.d.ts.map +1 -1
  24. package/dist/server/smtp/services/MailReceiverService.js +31 -17
  25. package/dist/server/smtp/services/MailReceiverService.js.map +1 -1
  26. package/dist/server/workflows/daily/DailyWorkflow.d.ts +2 -1
  27. package/dist/server/workflows/daily/DailyWorkflow.d.ts.map +1 -1
  28. package/dist/server/workflows/daily/DailyWorkflow.js +6 -2
  29. package/dist/server/workflows/daily/DailyWorkflow.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/src/server/model/entities/DomainReputation.ts +15 -0
  33. package/src/server/seed/ui/seed-ui.ts +1 -1
  34. package/src/server/services/EmailAddressService.ts +10 -0
  35. package/src/server/services/message-events/MessagingService.ts +3 -0
  36. package/src/server/smtp/services/ChannelService.ts +13 -7
  37. package/src/server/smtp/services/DomainReputationService.ts +44 -0
  38. package/src/server/smtp/services/MailReceiverService.ts +35 -21
  39. package/src/server/workflows/daily/DailyWorkflow.ts +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@social-mail/social-mail-web-server",
3
- "version": "1.7.261",
3
+ "version": "1.7.263",
4
4
  "description": "## Phase 1",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
+ }
@@ -3,7 +3,7 @@ import semver from "semver";
3
3
 
4
4
  export default async function seedUI(config: DBConfig) {
5
5
 
6
- const version = "1.4.175";
6
+ const version = "1.4.176";
7
7
 
8
8
  const existing = await config.getValue(UIPackageConfig, false);
9
9
  if (existing) {
@@ -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, channel: Mailbox) {
20
+ async getAccess({ senderID, senderAddress, recipientChannel }: { senderAddress?: string, senderID?: number, recipientChannel: Mailbox }) {
21
21
 
22
- if (!channel.isPrivateChannel && !channel.requiresMembershipApproval) {
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 } = channel;
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 (channel.isPrivateChannel) {
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 sender: EmailAddress;
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 sender = this.sender = await this.emailAddressService.getEmailAddressFrom(address.address);
133
- const { mailbox, domainID } = sender;
142
+ const domain = this.fromDomain = this.from.address.split("@",2).pop();
134
143
 
135
- const reputation = await this.db.domains.where({ domainID }, (p) => (x) => x.domainID === p.domainID)
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(domainID, "rcpt-not-found");
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(domainID, "mail-received");
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(this.sender.emailAddressID, mailbox);
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(domainID: number, activity: serverActivities) {
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
- .update(void 0, (p) => (x) => ({
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()))