mirror of
https://github.com/discourse/discourse.git
synced 2024-12-16 09:05:47 +08:00
ab260e70be
Adds Mailpace (formerly known as ohmystmp) webhook Co-authored-by: ruq <hosch@mailbox.org>
567 lines
20 KiB
Ruby
567 lines
20 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.describe WebhooksController do
|
|
before { Discourse.redis.flushdb }
|
|
|
|
let(:email) { "em@il.com" }
|
|
let(:message_id) { "12345@il.com" }
|
|
|
|
describe "#mailgun" do
|
|
let(:token) { "705a8ccd2ce932be8e98c221fe701c1b4a0afcb8bbd57726de" }
|
|
let(:timestamp) { Time.now.to_i }
|
|
let(:data) { "#{timestamp}#{token}" }
|
|
let(:signature) { OpenSSL::HMAC.hexdigest("SHA256", SiteSetting.mailgun_api_key, data) }
|
|
|
|
before do
|
|
SiteSetting.mailgun_api_key = "key-8221462f0c915af3f6f2e2df7aa5a493"
|
|
ActionController::Base.allow_forgery_protection = true # Ensure the endpoint works, even with CSRF protection generally enabled
|
|
end
|
|
|
|
after { ActionController::Base.allow_forgery_protection = false }
|
|
|
|
it "works (deprecated)" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailgun.json",
|
|
params: {
|
|
"token" => token,
|
|
"timestamp" => timestamp,
|
|
"event" => "dropped",
|
|
"recipient" => email,
|
|
"Message-Id" => "<#{message_id}>",
|
|
"signature" => signature,
|
|
"error" => "smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
|
|
"code" => "5.1.1",
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq("5.1.1")
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "works (new)" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailgun.json",
|
|
params: {
|
|
"signature" => {
|
|
"token" => token,
|
|
"timestamp" => timestamp,
|
|
"signature" => signature,
|
|
},
|
|
"event-data" => {
|
|
"event" => "failed",
|
|
"severity" => "temporary",
|
|
"recipient" => email,
|
|
"message" => {
|
|
"headers" => {
|
|
"message-id" => message_id,
|
|
},
|
|
},
|
|
},
|
|
"delivery-status" => {
|
|
"message" =>
|
|
"smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
|
|
"code" => "5.1.1",
|
|
"description" => "",
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq("5.1.1")
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
|
|
end
|
|
end
|
|
|
|
describe "#sendgrid" do
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/sendgrid.json",
|
|
params: {
|
|
"_json" => [
|
|
{
|
|
"email" => email,
|
|
"smtp-id" => "<12345@il.com>",
|
|
"event" => "bounce",
|
|
"status" => "5.0.0",
|
|
},
|
|
],
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq("5.0.0")
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "verifies signatures" do
|
|
SiteSetting.sendgrid_verification_key =
|
|
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g=="
|
|
|
|
post "/webhooks/sendgrid.json",
|
|
headers: {
|
|
"X-Twilio-Email-Event-Webhook-Signature" =>
|
|
"MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=",
|
|
"X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502",
|
|
},
|
|
params:
|
|
"[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>\",\"timestamp\":1600112492}]\r\n"
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "returns error if signature verification fails" do
|
|
SiteSetting.sendgrid_verification_key =
|
|
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g=="
|
|
|
|
post "/webhooks/sendgrid.json",
|
|
headers: {
|
|
"X-Twilio-Email-Event-Webhook-Signature" =>
|
|
"MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=",
|
|
"X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502",
|
|
},
|
|
params:
|
|
"[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>\",\"timestamp\":1600112492}]\r\n"
|
|
|
|
expect(response.status).to eq(406)
|
|
end
|
|
|
|
it "returns error if signature is invalid" do
|
|
SiteSetting.sendgrid_verification_key = "foo"
|
|
|
|
post "/webhooks/sendgrid.json",
|
|
headers: {
|
|
"X-Twilio-Email-Event-Webhook-Signature" =>
|
|
"MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=",
|
|
"X-Twilio-Email-Event-Webhook-Timestamp" => "1600112502",
|
|
},
|
|
params:
|
|
"[{\"email\":\"hello@world.com\",\"event\":\"dropped\",\"reason\":\"Bounced Address\",\"sg_event_id\":\"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA\",\"sg_message_id\":\"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0\",\"smtp-id\":\"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>\",\"timestamp\":1600112492}]\r\n"
|
|
|
|
expect(response.status).to eq(406)
|
|
end
|
|
end
|
|
|
|
describe "#mailjet" do
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailjet.json",
|
|
params: {
|
|
"event" => "bounce",
|
|
"email" => email,
|
|
"hard_bounce" => true,
|
|
"CustomID" => message_id,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq(nil) # mailjet doesn't give us this
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "verifies signatures" do
|
|
SiteSetting.mailjet_webhook_token = "foo"
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailjet.json?t=foo",
|
|
params: {
|
|
"event" => "bounce",
|
|
"email" => email,
|
|
"hard_bounce" => true,
|
|
"CustomID" => message_id,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(email_log.reload.bounced).to eq(true)
|
|
end
|
|
|
|
it "returns error if signature verification fails" do
|
|
SiteSetting.mailjet_webhook_token = "foo"
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailjet.json?t=bar",
|
|
params: {
|
|
"event" => "bounce",
|
|
"email" => email,
|
|
"hard_bounce" => true,
|
|
"CustomID" => message_id,
|
|
}
|
|
|
|
expect(response.status).to eq(406)
|
|
expect(email_log.reload.bounced).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "#mailpace" do
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailpace.json",
|
|
params: {
|
|
event: "email.bounced",
|
|
payload: {
|
|
status: "bounced",
|
|
to: email,
|
|
message_id: "<#{message_id}>",
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq(nil) # mailpace doesn't give us this
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "soft bounces" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mailpace.json",
|
|
params: {
|
|
event: "email.deferred",
|
|
payload: {
|
|
status: "deferred",
|
|
to: email,
|
|
message_id: "<#{message_id}>",
|
|
},
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq(nil) # mailpace doesn't give us this
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
|
|
end
|
|
end
|
|
|
|
describe "#mandrill" do
|
|
let(:payload) do
|
|
"mandrill_events=%5B%7B%22event%22%3A%22hard_bounce%22%2C%22msg%22%3A%7B%22email%22%3A%22em%40il.com%22%2C%22diag%22%3A%225.1.1%22%2C%22bounce_description%22%3A%22smtp%3B+550-5.1.1+The+email+account+that+you+tried+to+reach+does+not+exist.%22%2C%22metadata%22%3A%7B%22message_id%22%3A%2212345%40il.com%22%7D%7D%7D%5D"
|
|
end
|
|
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/mandrill.json",
|
|
params: {
|
|
mandrill_events: [
|
|
{
|
|
"event" => "hard_bounce",
|
|
"msg" => {
|
|
"email" => email,
|
|
"diag" => "5.1.1",
|
|
:"bounce_description" =>
|
|
"smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
|
|
"metadata" => {
|
|
"message_id" => message_id,
|
|
},
|
|
},
|
|
},
|
|
].to_json,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq("5.1.1")
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "verifies signatures" do
|
|
SiteSetting.mandrill_authentication_key = "wr_JeJNO9OI65RFDrvk3Zw"
|
|
|
|
post "/webhooks/mandrill.json",
|
|
headers: {
|
|
"X-Mandrill-Signature" => "Q5pCb903EjEqRZ99gZrlYKOfvIU=",
|
|
},
|
|
params: payload
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
|
|
it "returns error if signature verification fails" do
|
|
SiteSetting.mandrill_authentication_key = "wr_JeJNO9OI65RFDrvk3Zw"
|
|
|
|
post "/webhooks/mandrill.json", headers: { "X-Mandrill-Signature" => "foo" }, params: payload
|
|
|
|
expect(response.status).to eq(406)
|
|
end
|
|
|
|
it "returns error if signature is invalid" do
|
|
SiteSetting.mandrill_authentication_key = "foo"
|
|
|
|
post "/webhooks/mandrill.json",
|
|
headers: {
|
|
"X-Mandrill-Signature" => "Q5pCb903EjEqRZ99gZrlYKOfvIU=",
|
|
},
|
|
params: payload
|
|
|
|
expect(response.status).to eq(406)
|
|
end
|
|
end
|
|
|
|
describe "#mandrill_head" do
|
|
it "works" do
|
|
head "/webhooks/mandrill.json"
|
|
|
|
expect(response.status).to eq(200)
|
|
end
|
|
end
|
|
|
|
describe "#postmark" do
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/postmark.json",
|
|
params: {
|
|
"Type" => "HardBounce",
|
|
"MessageID" => message_id,
|
|
"Email" => email,
|
|
}
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq(nil) # postmark doesn't give us this
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "soft bounces" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/postmark.json",
|
|
params: {
|
|
"Type" => "SoftBounce",
|
|
"MessageID" => message_id,
|
|
"Email" => email,
|
|
}
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.bounce_error_code).to eq(nil) # postmark doesn't give us this
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
|
|
end
|
|
|
|
it "verifies signatures" do
|
|
SiteSetting.postmark_webhook_token = "foo"
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/postmark.json?t=foo",
|
|
params: {
|
|
"Type" => "HardBounce",
|
|
"MessageID" => message_id,
|
|
"Email" => email,
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(email_log.reload.bounced).to eq(true)
|
|
end
|
|
|
|
it "returns error if signature verification fails" do
|
|
SiteSetting.postmark_webhook_token = "foo"
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/postmark.json?t=bar",
|
|
params: {
|
|
"Type" => "HardBounce",
|
|
"MessageID" => message_id,
|
|
"Email" => email,
|
|
}
|
|
|
|
expect(response.status).to eq(406)
|
|
expect(email_log.reload.bounced).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "#sparkpost" do
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/sparkpost.json",
|
|
params: {
|
|
"_json" => [
|
|
{
|
|
"msys" => {
|
|
"message_event" => {
|
|
"bounce_class" => 10,
|
|
"error_code" => "554",
|
|
"rcpt_to" => email,
|
|
"rcpt_meta" => {
|
|
"message_id" => message_id,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
|
|
it "verifies signatures" do
|
|
SiteSetting.sparkpost_webhook_token = "foo"
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/sparkpost.json?t=foo",
|
|
params: {
|
|
"_json" => [
|
|
{
|
|
"msys" => {
|
|
"message_event" => {
|
|
"bounce_class" => 10,
|
|
"error_code" => "554",
|
|
"rcpt_to" => email,
|
|
"rcpt_meta" => {
|
|
"message_id" => message_id,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
expect(response.status).to eq(200)
|
|
expect(email_log.reload.bounced).to eq(true)
|
|
end
|
|
|
|
it "returns error if signature verification fails" do
|
|
SiteSetting.sparkpost_webhook_token = "foo"
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
post "/webhooks/sparkpost.json?t=bar",
|
|
params: {
|
|
"_json" => [
|
|
{
|
|
"msys" => {
|
|
"message_event" => {
|
|
"bounce_class" => 10,
|
|
"error_code" => "554",
|
|
"rcpt_to" => email,
|
|
"rcpt_meta" => {
|
|
"message_id" => message_id,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
expect(response.status).to eq(406)
|
|
expect(email_log.reload.bounced).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe "#aws" do
|
|
let(:payload) do
|
|
{
|
|
"Type" => "Notification",
|
|
"Message" => {
|
|
"notificationType" => "Bounce",
|
|
:"bounce" => {
|
|
"bounceType" => "Permanent",
|
|
"reportingMTA" => "dns; email.example.com",
|
|
:"bouncedRecipients" => [
|
|
{
|
|
"emailAddress" => email,
|
|
"status" => "5.1.1",
|
|
"action" => "failed",
|
|
"diagnosticCode" => "smtp; 550 5.1.1 <#{email}>... User",
|
|
},
|
|
],
|
|
"bounceSubType" => "General",
|
|
"timestamp" => "2016-01-27T14:59:38.237Z",
|
|
"feedbackId" => "00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa068a-000000",
|
|
"remoteMtaIp" => "127.0.2.0",
|
|
},
|
|
:"mail" => {
|
|
"timestamp" => "2016-01-27T14:59:38.237Z",
|
|
"source" => "john@example.com",
|
|
"sourceArn" => "arn:aws:ses:us-east-1:888888888888:identity/example.com",
|
|
"sourceIp" => "127.0.3.0",
|
|
"sendingAccountId" => "123456789012",
|
|
"callerIdentity" => "IAM_user_or_role_name",
|
|
"messageId" => message_id,
|
|
"destination" => [email, "jane@example.com", "mary@example.com", "richard@example.com"],
|
|
"headersTruncated" => false,
|
|
"headers" => [
|
|
{ "name" => "From", "value" => "\"John Doe\" <john@example.com>" },
|
|
{
|
|
"name" => "To",
|
|
"value" =>
|
|
"\"Test\" <#{email}>, \"Jane Doe\" <jane@example.com>, \"Mary Doe\" <mary@example.com>, \"Richard Doe\" <richard@example.com>",
|
|
},
|
|
{ "name" => "Message-ID", "value" => message_id },
|
|
{ "name" => "Subject", "value" => "Hello" },
|
|
{ "name" => "Content-Type", "value" => "text/plain; charset=\"UTF-8\"" },
|
|
{ "name" => "Content-Transfer-Encoding", "value" => "base64" },
|
|
{ "name" => "Date", "value" => "Wed, 27 Jan 2016 14:05:45 +0000" },
|
|
],
|
|
"commonHeaders" => {
|
|
"from" => ["John Doe <john@example.com>"],
|
|
"date" => "Wed, 27 Jan 2016 14:05:45 +0000",
|
|
"to" => [
|
|
"\"Test\" <#{email}>, Jane Doe <jane@example.com>, Mary Doe <mary@example.com>, Richard Doe <richard@example.com>",
|
|
],
|
|
"messageId" => message_id,
|
|
"subject" => "Hello",
|
|
},
|
|
},
|
|
}.to_json,
|
|
}.to_json
|
|
end
|
|
|
|
before { Jobs.run_immediately! }
|
|
|
|
it "works" do
|
|
user = Fabricate(:user, email: email)
|
|
email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email)
|
|
|
|
require "aws-sdk-sns"
|
|
Aws::SNS::MessageVerifier.any_instance.stubs(:authentic?).with(payload).returns(true)
|
|
|
|
post "/webhooks/aws.json", headers: { "RAW_POST_DATA" => payload }
|
|
expect(response.status).to eq(200)
|
|
|
|
email_log.reload
|
|
expect(email_log.bounced).to eq(true)
|
|
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.hard_bounce_score)
|
|
end
|
|
end
|
|
end
|