Skip to content

Commit 62890a3

Browse files
authored
Merge pull request #67 from amatsuda/multipart
Handle multipart mails
2 parents 5246fe6 + 5fdd584 commit 62890a3

File tree

9 files changed

+229
-34
lines changed

9 files changed

+229
-34
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class ActiveStorage::FileBlob < ApplicationRecord
2+
end

app/models/message.rb

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,83 @@ class Message < ApplicationRecord
1010
# https://blade.ruby-lang.org/ruby-talk/410000 is not.
1111
self.skip_time_zone_conversion_for_attributes = [:published_at]
1212

13+
has_many_attached :attachments
14+
1315
attr_accessor :children
1416

1517
class << self
1618
def from_mail(mail, list, list_seq)
17-
body = Kconv.toutf8 mail.body.raw_source
18-
if ((list.name == 'ruby-dev') && list_seq.in?([13859, 26229, 39731, 39734])) || ((list.name == 'ruby-core') && list_seq.in?([5231])) || ((list.name == 'ruby-list') && list_seq.in?([29637, 29711, 30148])) || ((list.name == 'ruby-talk') && list_seq.in?([5198, 61316]))
19-
body.gsub!("\u0000", '')
20-
end
21-
if (list.name == 'ruby-list') && list_seq.in?([37565, 38116, 43106])
22-
mail.header[:subject].value.chop!
23-
end
24-
if (list.name == 'ruby-list') && (list_seq.in?([41850, 43710]))
25-
mail.header[:subject].value = Kconv.toutf8 mail.header[:subject].value
26-
end
27-
subject = mail.subject
28-
subject = Kconv.toutf8 subject if subject
29-
from = Kconv.toutf8 mail.from_address&.raw
30-
if !from && (list.name == 'ruby-core') && (list_seq == 161)
31-
from = mail.from.encode Encoding::UTF_8, Encoding::KOI8_R
32-
end
19+
new.from_mail(mail, list, list_seq)
20+
end
21+
end
3322

34-
message_id = mail.message_id&.encode Encoding::UTF_8, invalid: :replace, undef: :replace
23+
def from_mail(mail, list, list_seq)
24+
self.list_id, self.list_seq, self.published_at = list.id, list_seq, mail.date
3525

36-
# mail.in_reply_to returns strange Array object in some cases (?), so let's use the raw value
37-
parent_message_id_header = extract_message_id_from_in_reply_to(mail.header[:in_reply_to]&.value)
38-
parent_message_id = Message.where(list_id: list.id, message_id_header: parent_message_id_header).pick(:id) if parent_message_id_header
39-
if !parent_message_id && (String === mail.references)
40-
parent_message_id = Message.where(list_id: list.id, message_id_header: mail.references).pick(:id)
41-
end
42-
if !parent_message_id && (Array === mail.references)
43-
mail.references.compact.each do |ref|
44-
break if (parent_message_id = Message.where(list_id: list.id, message_id_header: ref).pick(:id))
45-
end
26+
if mail.multipart?
27+
mail.parts.each do |p|
28+
handle_multipart p
4629
end
30+
else
31+
self.body = Kconv.toutf8 mail.body.raw_source
32+
end
33+
34+
if ((list.name == 'ruby-dev') && list_seq.in?([13859, 26229, 39731, 39734])) || ((list.name == 'ruby-core') && list_seq.in?([5231])) || ((list.name == 'ruby-list') && list_seq.in?([29637, 29711, 30148])) || ((list.name == 'ruby-talk') && list_seq.in?([5198, 61316]))
35+
self.body.gsub!("\u0000", '')
36+
end
37+
38+
if (list.name == 'ruby-list') && list_seq.in?([37565, 38116, 43106])
39+
mail.header[:subject].value.chop!
40+
end
41+
if (list.name == 'ruby-list') && (list_seq.in?([41850, 43710]))
42+
mail.header[:subject].value = Kconv.toutf8 mail.header[:subject].value
43+
end
44+
self.subject = mail.subject
45+
self.subject = Kconv.toutf8 subject if self.subject
4746

48-
new list_id: list.id, list_seq: list_seq, body: body, subject: subject, from: from, published_at: mail.date, message_id_header: message_id, parent_id: parent_message_id
47+
self.from = Kconv.toutf8 mail.from_address&.raw
48+
if !self.from && (list.name == 'ruby-core') && (list_seq == 161)
49+
self.from = mail.from.encode Encoding::UTF_8, Encoding::KOI8_R
4950
end
5051

51-
private def extract_message_id_from_in_reply_to(header)
52-
header && header.strip.scan(/<([^>]+)>/).flatten.first
52+
self.message_id_header = mail.message_id&.encode Encoding::UTF_8, invalid: :replace, undef: :replace
53+
54+
# mail.in_reply_to returns strange Array object in some cases (?), so let's use the raw value
55+
parent_message_id_header = extract_message_id_from_in_reply_to(mail.header[:in_reply_to]&.value)
56+
self.parent_id = Message.where(list_id: list.id, message_id_header: parent_message_id_header).pick(:id) if parent_message_id_header
57+
if !self.parent_id && (String === mail.references)
58+
self.parent_id = Message.where(list_id: list.id, message_id_header: mail.references).pick(:id)
59+
end
60+
if !self.parent_id && (Array === mail.references)
61+
mail.references.compact.each do |ref|
62+
break if (self.parent_id = Message.where(list_id: list.id, message_id_header: ref).pick(:id))
63+
end
5364
end
5465

66+
self
67+
end
68+
69+
private def handle_multipart(part)
70+
if part.attachment?
71+
file = StringIO.new(part.decoded)
72+
attachments.attach(io: file, filename: part.filename, content_type: part.content_type)
73+
else
74+
case part.content_type.downcase
75+
when /^text\/plain/
76+
(self.body ||= '') << Kconv.toutf8(part.body.raw_source)
77+
when /^text\/html;/
78+
(self.html_body ||= '') << Kconv.toutf8(part.body.raw_source)
79+
else
80+
puts "Unknown content_type: #{part.content_type}"
81+
end
82+
end
83+
end
84+
85+
private def extract_message_id_from_in_reply_to(header)
86+
header && header.strip.scan(/<([^>]+)>/).flatten.first
87+
end
88+
89+
class << self
5590
def from_s3(list_name, list_seq, s3_client = Aws::S3::Client.new(region: BLADE_BUCKET_REGION))
5691
obj = s3_client.get_object(bucket: BLADE_BUCKET_NAME, key: "#{list_name}/#{list_seq}")
5792

config/storage.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
test:
2-
service: Disk
3-
root: <%= Rails.root.join("tmp/storage") %>
2+
service: Database
43

54
local:
6-
service: Disk
7-
root: <%= Rails.root.join("storage") %>
5+
service: Database
86

97
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
108
# amazon:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddHtmlBodyToMessages < ActiveRecord::Migration[8.0]
2+
def change
3+
add_column :messages, :html_body, :text
4+
end
5+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class CreateActiveStorageFileBlobs < ActiveRecord::Migration[8.0]
2+
def change
3+
create_table :active_storage_file_blobs do |t|
4+
t.string :key
5+
t.binary :data
6+
7+
t.timestamps
8+
end
9+
add_index :active_storage_file_blobs, :key, unique: true
10+
end
11+
end
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# This migration comes from active_storage (originally 20170806125915)
2+
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
3+
def change
4+
# Use Active Record's configured type for primary and foreign keys
5+
primary_key_type, foreign_key_type = primary_and_foreign_key_types
6+
7+
create_table :active_storage_blobs, id: primary_key_type do |t|
8+
t.string :key, null: false
9+
t.string :filename, null: false
10+
t.string :content_type
11+
t.text :metadata
12+
t.string :service_name, null: false
13+
t.bigint :byte_size, null: false
14+
t.string :checksum
15+
16+
if connection.supports_datetime_with_precision?
17+
t.datetime :created_at, precision: 6, null: false
18+
else
19+
t.datetime :created_at, null: false
20+
end
21+
22+
t.index [ :key ], unique: true
23+
end
24+
25+
create_table :active_storage_attachments, id: primary_key_type do |t|
26+
t.string :name, null: false
27+
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
28+
t.references :blob, null: false, type: foreign_key_type
29+
30+
if connection.supports_datetime_with_precision?
31+
t.datetime :created_at, precision: 6, null: false
32+
else
33+
t.datetime :created_at, null: false
34+
end
35+
36+
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
37+
t.foreign_key :active_storage_blobs, column: :blob_id
38+
end
39+
40+
create_table :active_storage_variant_records, id: primary_key_type do |t|
41+
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
42+
t.string :variation_digest, null: false
43+
44+
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
45+
t.foreign_key :active_storage_blobs, column: :blob_id
46+
end
47+
end
48+
49+
private
50+
def primary_and_foreign_key_types
51+
config = Rails.configuration.generators
52+
setting = config.options[config.orm][:primary_key_type]
53+
primary_key_type = setting || :primary_key
54+
foreign_key_type = setting || :bigint
55+
[ primary_key_type, foreign_key_type ]
56+
end
57+
end

db/schema.rb

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require 'active_storage/service'
4+
5+
module ActiveStorage
6+
class Service::DatabaseService < Service
7+
def upload(key, io, checksum: nil, **)
8+
instrument :upload, key: key, checksum: checksum do
9+
ActiveStorage::FileBlob.find_or_initialize_by(key: key) do
10+
it.data = io.read
11+
it.save!
12+
end
13+
end
14+
end
15+
16+
def download(key)
17+
instrument :download, key: key do
18+
ActiveStorage::FileBlob.where(key: key).pick(:data)
19+
end
20+
end
21+
22+
def delete(key)
23+
instrument :delete, key: key do
24+
ActiveStorage::FileBlob.find_by(key: key)&.destroy
25+
end
26+
end
27+
28+
def exist?(key)
29+
instrument :exist, key: key do |payload|
30+
payload[:exist] = ActiveStorage::FileBlob.exists?(key: key)
31+
end
32+
end
33+
34+
private
35+
36+
def service_name
37+
'Database'
38+
end
39+
end
40+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "test_helper"
2+
3+
class ActiveStorageFileBlobTest < ActiveSupport::TestCase
4+
# test "the truth" do
5+
# assert true
6+
# end
7+
end

0 commit comments

Comments
 (0)