mail_room 0.9.1 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitlab-ci.yml +14 -12
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +501 -0
- data/.ruby-version +1 -1
- data/.travis.yml +15 -6
- data/CHANGELOG.md +25 -0
- data/README.md +137 -14
- data/Rakefile +1 -1
- data/lib/mail_room/arbitration/redis.rb +8 -15
- data/lib/mail_room/cli.rb +9 -2
- data/lib/mail_room/connection.rb +6 -160
- data/lib/mail_room/crash_handler.rb +26 -0
- data/lib/mail_room/delivery/letter_opener.rb +1 -1
- data/lib/mail_room/delivery/postback.rb +39 -7
- data/lib/mail_room/delivery/que.rb +4 -2
- data/lib/mail_room/delivery/sidekiq.rb +6 -4
- data/lib/mail_room/imap/connection.rb +200 -0
- data/lib/mail_room/imap/message.rb +19 -0
- data/lib/mail_room/imap.rb +8 -0
- data/lib/mail_room/logger/structured.rb +35 -0
- data/lib/mail_room/mailbox.rb +123 -28
- data/lib/mail_room/mailbox_watcher.rb +11 -1
- data/lib/mail_room/message.rb +16 -0
- data/lib/mail_room/microsoft_graph/connection.rb +217 -0
- data/lib/mail_room/microsoft_graph.rb +7 -0
- data/lib/mail_room/version.rb +1 -1
- data/lib/mail_room.rb +3 -1
- data/logfile.log +1 -0
- data/mail_room.gemspec +8 -4
- data/spec/fixtures/test_config.yml +2 -0
- data/spec/lib/arbitration/redis_spec.rb +44 -24
- data/spec/lib/cli_spec.rb +46 -11
- data/spec/lib/configuration_spec.rb +13 -10
- data/spec/lib/coordinator_spec.rb +11 -9
- data/spec/lib/crash_handler_spec.rb +42 -0
- data/spec/lib/delivery/letter_opener_spec.rb +10 -6
- data/spec/lib/delivery/logger_spec.rb +10 -12
- data/spec/lib/delivery/postback_spec.rb +77 -17
- data/spec/lib/delivery/que_spec.rb +6 -9
- data/spec/lib/delivery/sidekiq_spec.rb +33 -11
- data/spec/lib/imap/connection_spec.rb +61 -0
- data/spec/lib/imap/message_spec.rb +36 -0
- data/spec/lib/logger/structured_spec.rb +87 -0
- data/spec/lib/mailbox_spec.rb +109 -33
- data/spec/lib/mailbox_watcher_spec.rb +54 -41
- data/spec/lib/message_spec.rb +35 -0
- data/spec/lib/microsoft_graph/connection_spec.rb +190 -0
- data/spec/spec_helper.rb +21 -2
- metadata +92 -31
- data/lib/mail_room/backports/imap.rb +0 -33
- data/spec/lib/connection_spec.rb +0 -63
@@ -3,27 +3,31 @@ require 'mail_room/delivery/letter_opener'
|
|
3
3
|
|
4
4
|
describe MailRoom::Delivery::LetterOpener do
|
5
5
|
describe '#deliver' do
|
6
|
-
let(:mailbox) {
|
6
|
+
let(:mailbox) {build_mailbox(location: '/tmp/somewhere')}
|
7
7
|
let(:delivery_method) {stub(:deliver!)}
|
8
8
|
let(:mail) {stub}
|
9
9
|
|
10
10
|
before :each do
|
11
11
|
Mail.stubs(:read_from_string).returns(mail)
|
12
12
|
::LetterOpener::DeliveryMethod.stubs(:new).returns(delivery_method)
|
13
|
-
|
14
|
-
MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message')
|
15
13
|
end
|
16
14
|
|
17
15
|
it 'creates a new LetterOpener::DeliveryMethod' do
|
18
|
-
|
16
|
+
::LetterOpener::DeliveryMethod.expects(:new).with(location: '/tmp/somewhere').returns(delivery_method)
|
17
|
+
|
18
|
+
MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message')
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'parses the message string with Mail' do
|
22
|
-
|
22
|
+
::Mail.expects(:read_from_string).with('a message')
|
23
|
+
|
24
|
+
MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message')
|
23
25
|
end
|
24
26
|
|
25
27
|
it 'delivers the mail message' do
|
26
|
-
|
28
|
+
delivery_method.expects(:deliver!).with(mail)
|
29
|
+
|
30
|
+
MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message')
|
27
31
|
end
|
28
32
|
end
|
29
33
|
end
|
@@ -4,43 +4,41 @@ require 'mail_room/delivery/logger'
|
|
4
4
|
describe MailRoom::Delivery::Logger do
|
5
5
|
describe '#initialize' do
|
6
6
|
context "without a log path" do
|
7
|
-
let(:mailbox) {
|
7
|
+
let(:mailbox) {build_mailbox}
|
8
8
|
|
9
9
|
it 'creates a new ruby logger' do
|
10
10
|
::Logger.stubs(:new)
|
11
11
|
|
12
|
-
|
12
|
+
::Logger.expects(:new).with(STDOUT)
|
13
13
|
|
14
|
-
|
14
|
+
MailRoom::Delivery::Logger.new(mailbox)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
18
|
context "with a log path" do
|
19
|
-
let(:mailbox) {
|
19
|
+
let(:mailbox) {build_mailbox(log_path: '/var/log/mail-room.log')}
|
20
20
|
|
21
21
|
it 'creates a new file to append to' do
|
22
|
-
::Logger.stubs(:new)
|
23
22
|
file = stub(:sync=)
|
24
|
-
::File.stubs(:open).returns(file)
|
25
23
|
|
26
|
-
|
24
|
+
File.expects(:open).with('/var/log/mail-room.log', 'a').returns(file)
|
25
|
+
::Logger.stubs(:new).with(file)
|
27
26
|
|
28
|
-
|
29
|
-
expect(::Logger).to have_received(:new).with(file)
|
27
|
+
MailRoom::Delivery::Logger.new(mailbox)
|
30
28
|
end
|
31
29
|
end
|
32
30
|
end
|
33
31
|
|
34
32
|
describe '#deliver' do
|
35
|
-
let(:mailbox) {
|
33
|
+
let(:mailbox) {build_mailbox}
|
36
34
|
|
37
35
|
it 'writes the message to info' do
|
38
36
|
logger = stub(:info)
|
39
37
|
::Logger.stubs(:new).returns(logger)
|
40
38
|
|
41
|
-
|
39
|
+
logger.expects(:info).with('a message')
|
42
40
|
|
43
|
-
|
41
|
+
MailRoom::Delivery::Logger.new(mailbox).deliver('a message')
|
44
42
|
end
|
45
43
|
end
|
46
44
|
end
|
@@ -3,29 +3,89 @@ require 'mail_room/delivery/postback'
|
|
3
3
|
|
4
4
|
describe MailRoom::Delivery::Postback do
|
5
5
|
describe '#deliver' do
|
6
|
-
|
7
|
-
:
|
8
|
-
|
9
|
-
|
6
|
+
context 'with token auth delivery' do
|
7
|
+
let(:mailbox) {build_mailbox({
|
8
|
+
delivery_url: 'http://localhost/inbox',
|
9
|
+
delivery_token: 'abcdefg'
|
10
|
+
})}
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
Faraday.stubs(:new).returns(connection)
|
12
|
+
let(:delivery_options) {
|
13
|
+
MailRoom::Delivery::Postback::Options.new(mailbox)
|
14
|
+
}
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
it 'posts the message with faraday' do
|
17
|
+
connection = stub
|
18
|
+
request = stub
|
19
|
+
Faraday.stubs(:new).returns(connection)
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
+
connection.expects(:token_auth).with('abcdefg')
|
22
|
+
connection.expects(:post).yields(request)
|
21
23
|
|
22
|
-
|
24
|
+
request.expects(:url).with('http://localhost/inbox')
|
25
|
+
request.expects(:body=).with('a message')
|
23
26
|
|
24
|
-
|
25
|
-
|
27
|
+
MailRoom::Delivery::Postback.new(delivery_options).deliver('a message')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with basic auth delivery options' do
|
32
|
+
let(:mailbox) {build_mailbox({
|
33
|
+
delivery_options: {
|
34
|
+
url: 'http://localhost/inbox',
|
35
|
+
username: 'user1',
|
36
|
+
password: 'password123abc'
|
37
|
+
}
|
38
|
+
})}
|
39
|
+
|
40
|
+
let(:delivery_options) {
|
41
|
+
MailRoom::Delivery::Postback::Options.new(mailbox)
|
42
|
+
}
|
43
|
+
|
44
|
+
it 'posts the message with faraday' do
|
45
|
+
connection = stub
|
46
|
+
request = stub
|
47
|
+
Faraday.stubs(:new).returns(connection)
|
48
|
+
|
49
|
+
connection.expects(:basic_auth).with('user1', 'password123abc')
|
50
|
+
connection.expects(:post).yields(request)
|
51
|
+
|
52
|
+
request.expects(:url).with('http://localhost/inbox')
|
53
|
+
request.expects(:body=).with('a message')
|
54
|
+
|
55
|
+
MailRoom::Delivery::Postback.new(delivery_options).deliver('a message')
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with content type in the delivery options' do
|
59
|
+
let(:mailbox) {build_mailbox({
|
60
|
+
delivery_options: {
|
61
|
+
url: 'http://localhost/inbox',
|
62
|
+
username: 'user1',
|
63
|
+
password: 'password123abc',
|
64
|
+
content_type: 'text/plain'
|
65
|
+
}
|
66
|
+
})}
|
67
|
+
|
68
|
+
|
69
|
+
let(:delivery_options) {
|
70
|
+
MailRoom::Delivery::Postback::Options.new(mailbox)
|
71
|
+
}
|
72
|
+
|
73
|
+
it 'posts the message with faraday' do
|
74
|
+
connection = stub
|
75
|
+
request = stub
|
76
|
+
Faraday.stubs(:new).returns(connection)
|
77
|
+
|
78
|
+
connection.expects(:post).yields(request)
|
79
|
+
request.stubs(:url)
|
80
|
+
request.stubs(:body=)
|
81
|
+
request.stubs(:headers).returns({})
|
82
|
+
connection.expects(:basic_auth).with('user1', 'password123abc')
|
26
83
|
|
27
|
-
|
28
|
-
|
84
|
+
MailRoom::Delivery::Postback.new(delivery_options).deliver('a message')
|
85
|
+
|
86
|
+
expect(request.headers['Content-Type']).to eq('text/plain')
|
87
|
+
end
|
88
|
+
end
|
29
89
|
end
|
30
90
|
end
|
31
91
|
end
|
@@ -3,7 +3,7 @@ require 'mail_room/delivery/que'
|
|
3
3
|
|
4
4
|
describe MailRoom::Delivery::Que do
|
5
5
|
describe '#deliver' do
|
6
|
-
let(:mailbox) {
|
6
|
+
let(:mailbox) {build_mailbox({
|
7
7
|
delivery_options: {
|
8
8
|
database: 'delivery_test',
|
9
9
|
username: 'postgres',
|
@@ -18,20 +18,15 @@ describe MailRoom::Delivery::Que do
|
|
18
18
|
let(:options) {MailRoom::Delivery::Que::Options.new(mailbox)}
|
19
19
|
|
20
20
|
it 'stores the message in que_jobs table' do
|
21
|
-
PG.
|
22
|
-
connection.stubs(:exec)
|
23
|
-
|
24
|
-
MailRoom::Delivery::Que.new(options).deliver('email')
|
25
|
-
|
26
|
-
expect(PG).to have_received(:connect).with({
|
21
|
+
PG.expects(:connect).with({
|
27
22
|
host: 'localhost',
|
28
23
|
port: 5432,
|
29
24
|
dbname: 'delivery_test',
|
30
25
|
user: 'postgres',
|
31
26
|
password: ''
|
32
|
-
})
|
27
|
+
}).returns(connection)
|
33
28
|
|
34
|
-
|
29
|
+
connection.expects(:exec).with(
|
35
30
|
"INSERT INTO que_jobs (priority, job_class, queue, args) VALUES ($1, $2, $3, $4)",
|
36
31
|
[
|
37
32
|
5,
|
@@ -40,6 +35,8 @@ describe MailRoom::Delivery::Que do
|
|
40
35
|
JSON.dump(['email'])
|
41
36
|
]
|
42
37
|
)
|
38
|
+
|
39
|
+
MailRoom::Delivery::Que.new(options).deliver('email')
|
43
40
|
end
|
44
41
|
end
|
45
42
|
end
|
@@ -7,31 +7,53 @@ describe MailRoom::Delivery::Sidekiq do
|
|
7
7
|
let(:options) { MailRoom::Delivery::Sidekiq::Options.new(mailbox) }
|
8
8
|
|
9
9
|
describe '#options' do
|
10
|
-
let(:redis_url) { 'redis://
|
10
|
+
let(:redis_url) { 'redis://localhost' }
|
11
|
+
let(:redis_options) { { redis_url: redis_url } }
|
11
12
|
|
12
13
|
context 'when only redis_url is specified' do
|
13
14
|
let(:mailbox) {
|
14
|
-
|
15
|
+
build_mailbox(
|
15
16
|
delivery_method: :sidekiq,
|
16
|
-
delivery_options:
|
17
|
-
redis_url: redis_url
|
18
|
-
}
|
17
|
+
delivery_options: redis_options
|
19
18
|
)
|
20
19
|
}
|
21
20
|
|
22
|
-
|
23
|
-
|
21
|
+
context 'with simple redis url' do
|
22
|
+
it 'client has same specified redis_url' do
|
23
|
+
expect(redis.client.options[:url]).to eq(redis_url)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'client is a instance of RedisNamespace class' do
|
27
|
+
expect(redis).to be_a ::Redis
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'connection has correct values' do
|
31
|
+
expect(redis.connection[:host]).to eq('localhost')
|
32
|
+
expect(redis.connection[:db]).to eq(0)
|
33
|
+
end
|
24
34
|
end
|
25
35
|
|
26
|
-
|
27
|
-
|
36
|
+
context 'with redis_db specified in options' do
|
37
|
+
before do
|
38
|
+
redis_options[:redis_db] = 4
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'client has correct redis_url' do
|
42
|
+
expect(redis.client.options[:url]).to eq(redis_url)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
it 'connection has correct values' do
|
47
|
+
expect(redis.connection[:host]).to eq('localhost')
|
48
|
+
expect(redis.connection[:db]).to eq(4)
|
49
|
+
end
|
28
50
|
end
|
29
51
|
end
|
30
52
|
|
31
53
|
context 'when namespace is specified' do
|
32
54
|
let(:namespace) { 'sidekiq_mailman' }
|
33
55
|
let(:mailbox) {
|
34
|
-
|
56
|
+
build_mailbox(
|
35
57
|
delivery_method: :sidekiq,
|
36
58
|
delivery_options: {
|
37
59
|
redis_url: redis_url,
|
@@ -53,7 +75,7 @@ describe MailRoom::Delivery::Sidekiq do
|
|
53
75
|
let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' }
|
54
76
|
let(:sentinels) { [{ host: '10.0.0.1', port: '26379' }] }
|
55
77
|
let(:mailbox) {
|
56
|
-
|
78
|
+
build_mailbox(
|
57
79
|
delivery_method: :sidekiq,
|
58
80
|
delivery_options: {
|
59
81
|
redis_url: redis_url,
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MailRoom::IMAP::Connection do
|
4
|
+
let(:imap) {stub}
|
5
|
+
let(:mailbox) {build_mailbox(delete_after_delivery: true, expunge_deleted: true)}
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Net::IMAP.stubs(:new).returns(imap)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "with imap set up" do
|
12
|
+
let(:connection) {MailRoom::IMAP::Connection.new(mailbox)}
|
13
|
+
let(:uid) { 1 }
|
14
|
+
let(:seqno) { 8 }
|
15
|
+
|
16
|
+
before :each do
|
17
|
+
imap.stubs(:starttls)
|
18
|
+
imap.stubs(:login)
|
19
|
+
imap.stubs(:select)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "is logged in" do
|
23
|
+
expect(connection.logged_in?).to eq(true)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "is not idling" do
|
27
|
+
expect(connection.idling?).to eq(false)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "is not disconnected" do
|
31
|
+
imap.stubs(:disconnected?).returns(false)
|
32
|
+
|
33
|
+
expect(connection.disconnected?).to eq(false)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "is ready to idle" do
|
37
|
+
expect(connection.ready_to_idle?).to eq(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "waits for a message to process" do
|
41
|
+
new_message = MailRoom::IMAP::Message.new(uid: uid, body: 'a message', seqno: seqno)
|
42
|
+
|
43
|
+
connection.on_new_message do |message|
|
44
|
+
expect(message).to eq(new_message)
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
attr = { 'UID' => uid, 'RFC822' => new_message.body }
|
49
|
+
fetch_data = Net::IMAP::FetchData.new(seqno, attr)
|
50
|
+
|
51
|
+
imap.expects(:idle)
|
52
|
+
imap.stubs(:uid_search).with(mailbox.search_command).returns([], [uid])
|
53
|
+
imap.expects(:uid_fetch).with([uid], "RFC822").returns([fetch_data])
|
54
|
+
mailbox.expects(:deliver?).with(uid).returns(true)
|
55
|
+
imap.expects(:store).with(seqno, "+FLAGS", [Net::IMAP::DELETED])
|
56
|
+
imap.expects(:expunge).once
|
57
|
+
|
58
|
+
connection.wait
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal:true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
describe MailRoom::IMAP::Message do
|
7
|
+
let(:uid) { SecureRandom.hex }
|
8
|
+
let(:body) { 'hello world' }
|
9
|
+
let(:seqno) { 5 }
|
10
|
+
|
11
|
+
subject { described_class.new(uid: uid, body: body, seqno: seqno) }
|
12
|
+
|
13
|
+
describe '#initalize' do
|
14
|
+
it 'initializes with required parameters' do
|
15
|
+
subject
|
16
|
+
|
17
|
+
expect(subject.uid).to eq(uid)
|
18
|
+
expect(subject.body).to eq(body)
|
19
|
+
expect(subject.seqno).to eq(seqno)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#==' do
|
24
|
+
let(:dup) { described_class.new(uid: uid, body: body, seqno: seqno) }
|
25
|
+
let(:base_msg) { MailRoom::Message.new(uid: uid, body: body) }
|
26
|
+
|
27
|
+
it 'matches an equivalent message' do
|
28
|
+
expect(dup == subject).to be true
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'does not match a base message' do
|
32
|
+
expect(subject == base_msg).to be false
|
33
|
+
expect(base_msg == subject).to be false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MailRoom::Logger::Structured do
|
4
|
+
|
5
|
+
subject { described_class.new $stdout }
|
6
|
+
|
7
|
+
let!(:now) { Time.now }
|
8
|
+
let(:timestamp) { now.to_datetime.iso8601(3) }
|
9
|
+
let(:message) { { action: 'exciting development', message: 'testing 123' } }
|
10
|
+
|
11
|
+
before do
|
12
|
+
Time.stubs(:now).returns(now)
|
13
|
+
end
|
14
|
+
|
15
|
+
[:debug, :info, :warn, :error, :fatal].each do |level|
|
16
|
+
it "logs #{level}" do
|
17
|
+
expect { subject.send(level, message) }.to output(json_matching(level.to_s.upcase, message)).to_stdout_from_any_process
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'logs unknown' do
|
22
|
+
expect { subject.unknown(message) }.to output(json_matching("ANY", message)).to_stdout_from_any_process
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'only accepts hashes' do
|
26
|
+
expect { subject.unknown("just a string!") }.to raise_error(ArgumentError, /must be a Hash/)
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'logging a hash as a message' do
|
30
|
+
it 'merges the contents' do
|
31
|
+
input = {
|
32
|
+
additional_field: "some value"
|
33
|
+
}
|
34
|
+
expected = {
|
35
|
+
severity: 'DEBUG',
|
36
|
+
time: timestamp,
|
37
|
+
additional_field: "some value"
|
38
|
+
}
|
39
|
+
|
40
|
+
expect { subject.debug(input) }.to output(as_regex(expected)).to_stdout_from_any_process
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#format_message' do
|
45
|
+
shared_examples 'timestamp formatting' do
|
46
|
+
it 'outputs ISO8601 timestamps' do
|
47
|
+
data = JSON.parse(subject.format_message('debug', input_timestamp, 'test', { message: 'hello' } ))
|
48
|
+
|
49
|
+
expect(data['time']).to eq(expected_timestamp)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with no timestamp' do
|
54
|
+
let(:input_timestamp) { nil }
|
55
|
+
let(:expected_timestamp) { timestamp }
|
56
|
+
|
57
|
+
it_behaves_like 'timestamp formatting'
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with DateTime' do
|
61
|
+
let(:input_timestamp) { now.to_datetime }
|
62
|
+
let(:expected_timestamp) { timestamp }
|
63
|
+
|
64
|
+
it_behaves_like 'timestamp formatting'
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with string' do
|
68
|
+
let(:input_timestamp) { now.to_s }
|
69
|
+
let(:expected_timestamp) { input_timestamp }
|
70
|
+
|
71
|
+
it_behaves_like 'timestamp formatting'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def json_matching(level, message)
|
76
|
+
contents = {
|
77
|
+
severity: level,
|
78
|
+
time: timestamp
|
79
|
+
}.merge(message)
|
80
|
+
|
81
|
+
as_regex(contents)
|
82
|
+
end
|
83
|
+
|
84
|
+
def as_regex(contents)
|
85
|
+
/#{Regexp.quote(contents.to_json)}/
|
86
|
+
end
|
87
|
+
end
|