Skip to content

Commit d5989e9

Browse files
committed
Email simulator script
This commit adds two scripts intended to simulate currently ongoing discussions.
1 parent c698431 commit d5989e9

File tree

5 files changed

+164
-2
lines changed

5 files changed

+164
-2
lines changed

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
COMPOSE ?= docker compose -f docker-compose.dev.yml
22

3-
.PHONY: dev dev-detach down shell console test imap logs db-migrate db-reset psql
3+
.PHONY: dev dev-detach down shell console test imap logs db-migrate db-reset psql sim-email-once sim-email-stream
44

55
dev: ## Start dev stack (foreground)
66
$(COMPOSE) up --build
@@ -34,3 +34,9 @@ imap: ## Start stack with IMAP worker profile
3434

3535
logs: ## Follow web logs
3636
$(COMPOSE) logs -f web
37+
38+
sim-email-once: ## Send a single simulated email (env: SENT_OFFSET_SECONDS, EXISTING_ALIAS_PROB, EXISTING_TOPIC_PROB)
39+
$(COMPOSE) exec web ruby script/simulate_email_once.rb
40+
41+
sim-email-stream: ## Start a continuous simulated email stream (env: MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS, EXISTING_ALIAS_PROB, EXISTING_TOPIC_PROB)
42+
$(COMPOSE) exec web ruby script/simulate_email_stream.rb

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,19 @@ Makefile shortcuts:
3131
* `make db-migrate` / `make db-reset`
3232
* `make psql`
3333

34-
Optional IMAP worker (off by default):
34+
### Incoming email simulator
35+
36+
There are two helper scripts `script/simulate_email_once.rb` and `simulate_email_stream.rb` that simulate incoming emails.
37+
The scripts can be configured by a few environment variables, for details see the source of the scripts.
38+
39+
Makefile shortcuts:
40+
* `make sim-email-once`
41+
* `make sim-email-stream`
42+
43+
### IMAP worker
44+
45+
The "production" IMAP worker which pulls actual mailing list messages from an IMAP label can be also run locally.
46+
3547
```bash
3648
docker compose -f docker-compose.dev.yml --profile imap up --build
3749
```

lib/dev/email_simulator.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# frozen_string_literal: true
2+
3+
require "securerandom"
4+
require "faker"
5+
6+
module Dev
7+
class EmailSimulator
8+
DEFAULT_EXISTING_ALIAS_PROB = 0.9
9+
DEFAULT_EXISTING_TOPIC_PROB = 0.9
10+
11+
def initialize(existing_alias_prob: DEFAULT_EXISTING_ALIAS_PROB, existing_topic_prob: DEFAULT_EXISTING_TOPIC_PROB)
12+
@existing_alias_prob = existing_alias_prob
13+
@existing_topic_prob = existing_topic_prob
14+
end
15+
16+
def generate_mail(sent_at: Time.current)
17+
from_alias = pick_alias
18+
topic, reply_to = pick_topic
19+
20+
mail = Mail.new
21+
mail.date = sent_at
22+
mail.message_id = "<sim-#{SecureRandom.hex}@hackorum.dev>"
23+
mail.from = "#{from_alias.name} <#{from_alias.email}>"
24+
mail.to = to_addresses(reply_to, topic)
25+
mail.subject = subject_for(topic, reply_to)
26+
27+
if reply_to&.message_id.present?
28+
mail.in_reply_to = reply_to.message_id
29+
mail.references = reply_to.message_id
30+
end
31+
32+
mail.body = body_for(reply_to)
33+
mail
34+
end
35+
36+
def ingest!(mail)
37+
EmailIngestor.new.ingest_raw(mail.to_s)
38+
end
39+
40+
def generate_and_ingest!(sent_at: Time.current)
41+
ingest!(generate_mail(sent_at: sent_at))
42+
end
43+
44+
private
45+
46+
def pick_alias
47+
use_existing = rand < @existing_alias_prob
48+
existing = Alias.order("RANDOM()").first if use_existing
49+
return existing if existing
50+
51+
Alias.create!(
52+
name: Faker::Name.name,
53+
email: Faker::Internet.email,
54+
primary_alias: false,
55+
verified_at: Time.current
56+
)
57+
end
58+
59+
def pick_topic
60+
use_existing = rand < @existing_topic_prob
61+
existing_topic = Topic.joins(:messages).order("RANDOM()").first if use_existing
62+
return [existing_topic, existing_topic&.messages&.order("RANDOM()")&.first] if existing_topic
63+
64+
[nil, nil]
65+
end
66+
67+
def subject_for(topic, reply_to)
68+
return "Re: #{topic.title}" if topic && reply_to
69+
return topic.title if topic
70+
Faker::Hacker.say_something_smart.capitalize
71+
end
72+
73+
def body_for(reply_to)
74+
paragraphs = Faker::Lorem.paragraphs(number: rand(2..5))
75+
body = paragraphs.join("\n\n")
76+
if reply_to
77+
quoted = reply_to.body.to_s.lines.first(4).map { |l| "> #{l.chomp}" }.join("\n")
78+
body = "#{quoted}\n\n#{body}"
79+
end
80+
body
81+
end
82+
83+
def to_addresses(reply_to, topic)
84+
recipients = []
85+
if reply_to
86+
recipients << reply_to.sender.email
87+
elsif topic
88+
recipients << topic.creator.email
89+
else
90+
recipients << Faker::Internet.email
91+
end
92+
recipients
93+
end
94+
end
95+
end

script/simulate_email_once.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative "../config/environment"
5+
require_relative "../lib/dev/email_simulator"
6+
7+
offset_seconds = ENV.fetch("SENT_OFFSET_SECONDS", "5").to_i
8+
existing_alias_prob = ENV.fetch("EXISTING_ALIAS_PROB", "0.9").to_f
9+
existing_topic_prob = ENV.fetch("EXISTING_TOPIC_PROB", "0.9").to_f
10+
11+
simulator = Dev::EmailSimulator.new(
12+
existing_alias_prob: existing_alias_prob,
13+
existing_topic_prob: existing_topic_prob
14+
)
15+
16+
sent_at = Time.current - offset_seconds
17+
msg = simulator.generate_mail(sent_at: sent_at)
18+
simulator.ingest!(msg)
19+
20+
puts "Injected simulated email at #{sent_at} with subject: #{msg.subject}"

script/simulate_email_stream.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require_relative "../config/environment"
5+
require_relative "../lib/dev/email_simulator"
6+
7+
min_interval = ENV.fetch("MIN_INTERVAL_SECONDS", "30").to_i
8+
max_interval = ENV.fetch("MAX_INTERVAL_SECONDS", "90").to_i
9+
existing_alias_prob = ENV.fetch("EXISTING_ALIAS_PROB", "0.9").to_f
10+
existing_topic_prob = ENV.fetch("EXISTING_TOPIC_PROB", "0.9").to_f
11+
12+
raise ArgumentError, "MIN_INTERVAL_SECONDS must be > 0" if min_interval <= 0
13+
raise ArgumentError, "MAX_INTERVAL_SECONDS must be >= MIN_INTERVAL_SECONDS" if max_interval < min_interval
14+
15+
simulator = Dev::EmailSimulator.new(
16+
existing_alias_prob: existing_alias_prob,
17+
existing_topic_prob: existing_topic_prob
18+
)
19+
20+
puts "Starting email stream simulator (interval #{min_interval}-#{max_interval}s, existing alias prob #{existing_alias_prob}, existing topic prob #{existing_topic_prob})"
21+
22+
loop do
23+
sent_at = Time.current - rand(1..10)
24+
mail = simulator.generate_mail(sent_at: sent_at)
25+
simulator.ingest!(mail)
26+
puts "[#{Time.current}] Injected simulated email with subject: #{mail.subject}"
27+
28+
sleep rand(min_interval..max_interval)
29+
end

0 commit comments

Comments
 (0)