Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attachments to email from MSGraph #770

Merged
merged 7 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ RSpec/AnyInstance:
- 'spec/requests/self-serve/api/contentful/pages_controller_spec.rb'
- 'spec/services/support/incoming_emails/shared_mailbox_spec.rb'
- 'spec/services/support/incoming_emails/case_assignment_spec.rb'
- 'spec/services/support/incoming_emails/email_attachments_spec.rb'

# Offense count: 20
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
Expand All @@ -37,6 +38,7 @@ RSpec/VerifiedDoubles:
- 'spec/jobs/support/synchronize_shared_inbox_job_spec.rb'
- 'spec/models/support/email_spec.rb'
- 'spec/services/support/incoming_emails/case_assignment_spec.rb'
- 'spec/models/support/email_attachment_spec.rb'

# Offense count: 3
# Configuration parameters: Database, Include.
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ GEM
zeitwerk (2.5.3)

PLATFORMS
arm64-darwin-20
arm64-darwin-21
x86_64-darwin-20
x86_64-linux
Expand Down
2 changes: 1 addition & 1 deletion app/forms/support/case_contracts_form_schema.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Support
class CaseContractsFormSchema < Schema
class CaseContractsFormSchema < ::Support::Schema
config.messages.top_namespace = :case_contracts_form

params do
Expand Down
2 changes: 1 addition & 1 deletion app/forms/support/case_hub_migration_form_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Support
#
# Validate "create a new case" form details for a Hub migration case
#
class CaseHubMigrationFormSchema < Schema
class CaseHubMigrationFormSchema < ::Support::Schema
config.messages.top_namespace = :case_migration_form

params do
Expand Down
2 changes: 1 addition & 1 deletion app/forms/support/case_procurement_details_form_schema.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Support
class CaseProcurementDetailsFormSchema < Schema
class CaseProcurementDetailsFormSchema < ::Support::Schema
config.messages.top_namespace = :case_procurement_details_form

params do
Expand Down
18 changes: 18 additions & 0 deletions app/jobs/support/get_email_attachments_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Support
#
# Pull attachment from MS graph, upload to active storage and persist metadata
#
class GetEmailAttachmentsJob < ApplicationJob
queue_as :support

def perform(email_id)
IncomingEmails::EmailAttachments.download(email: get_email(email_id))
end

private

def get_email(email_id)
Support::Email.find(email_id)
end
end
end
2 changes: 2 additions & 0 deletions app/models/support/email.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def import_from_message(message, folder: :inbox)
}
end,
)
# perform later
GetEmailAttachmentsJob.perform_later(id) if message.has_attachments
end

def automatically_assign_case
Expand Down
14 changes: 14 additions & 0 deletions app/models/support/email_attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ class EmailAttachment < ApplicationRecord

scope :inline, -> { where(is_inline: true) }

def self.import_attachment(attachment, email)
email_attachment = email.attachments.find_or_initialize_by(outlook_id: attachment.id)
email_attachment.import_from_ms_attachment(attachment)
end

def import_from_ms_attachment(attachment)
Tempfile.create(attachment.name, binmode: true) do |f|
f.write(Base64.decode64(attachment.content_bytes))
f.rewind
file.attach(io: f, filename: attachment.name)
save!
end
end

private

def update_file_attributes
Expand Down
25 changes: 25 additions & 0 deletions app/services/support/incoming_emails/email_attachments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Support
module IncomingEmails
class EmailAttachments
def self.download(email:, graph_client: MicrosoftGraph.client)
email_attachments = EmailAttachments.new(graph_client: graph_client, email: email)
email_attachments.for_message.each do |attachment|
Support::EmailAttachment.import_attachment(attachment, email)
end
end

attr_reader :graph_client, :email

def initialize(graph_client:, email:)
@graph_client = graph_client
@email = email
end

def for_message
graph_client.get_file_attachments(SHARED_MAILBOX_USER_ID, email.outlook_id)
end
end
end
end
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@

if Rails.env.development?
require "sidekiq/web"
mount Sidekiq::Web => "/sidekiq"
mount Sidekiq::Web, at: "/sidekiq"
end

#
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddOutlookIdToEmailAttachment < ActiveRecord::Migration[6.1]
def change
add_column :support_email_attachments, :outlook_id, :string, index: true
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_01_12_131559) do
ActiveRecord::Schema.define(version: 2022_01_17_121023) do

# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
Expand Down Expand Up @@ -288,6 +288,7 @@
t.datetime "updated_at", precision: 6, null: false
t.boolean "is_inline", default: false
t.string "content_id"
t.string "outlook_id"
end

create_table "support_emails", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
Expand Down
7 changes: 7 additions & 0 deletions lib/microsoft_graph/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def mark_message_as_read(user_id, mail_folder, message_ms_id)
client_session.graph_api_patch("users/#{user_id}/mailFolders('#{mail_folder}')/messages/#{message_ms_id}", body)
end

# https://docs.microsoft.com/en-us/graph/api/message-list-attachments?view=graph-rest-1.0&tabs=http
def get_file_attachments(user_id, message_ms_id)
json = client_session.graph_api_get("users/#{user_id}/messages/#{message_ms_id}/attachments")
file_attachments = json["value"].select { |item| item["@odata.type"] == "#microsoft.graph.fileAttachment" }
Transformer::Attachment.transform_collection(file_attachments, into: Resource::Attachment)
end

private

def format_query(query_parts)
Expand Down
2 changes: 1 addition & 1 deletion lib/microsoft_graph/resource/attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Attachment
extend Dry::Initializer

option :content_bytes, Types::String
option :content_id, Types::String
option :content_id, Types::String | Types::Nil, optional: true
option :content_type, Types::String
option :id, Types::String
option :is_inline, Types::Bool
Expand Down
4 changes: 4 additions & 0 deletions spec/factories/support/email_attachments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
file { Rack::Test::UploadedFile.new(Rails.root.join("spec/support/assets/support/email_attachments/attachment.txt"), "text/plain") }

association :email, factory: :support_email

trait :without_file do
file { nil }
end
end
end
16 changes: 16 additions & 0 deletions spec/jobs/support/get_email_attachments_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
describe Support::GetEmailAttachmentsJob do
subject(:job) { described_class.new }

let(:email) { create(:support_email) }

describe "#perform" do
it "downloads email attachments for given email id" do
allow(Support::IncomingEmails::EmailAttachments).to receive(:download)

job.perform(email.id)

expect(Support::IncomingEmails::EmailAttachments).to have_received(:download)
.with(email: email).once
end
end
end
48 changes: 48 additions & 0 deletions spec/lib/microsoft_graph/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,52 @@
expect(client.mark_message_as_read(user_id, mail_folder, message_id)).to eql(graph_api_response)
end
end

describe "#get_file_attachments" do
let(:message_id) { "MESSAGE_ID" }

let(:graph_api_response) do
{
"value" => [
{ "@odata.type" => "#microsoft.graph.fileAttachment",
"contentType": "contentType-value",
"contentLocation": "contentLocation-value",
"contentBytes": "contentBytes-value",
"contentId": "null",
"lastModifiedDateTime": "datetime-value",
"id": "id-value",
"isInline": false,
"name": "example-file-1",
"size": 99 },
{ "@odata.type" => "#microsoft.graph.fileAttachment",
"contentType": "contentType-value",
"contentLocation": "contentLocation-value",
"contentBytes": "contentBytes-value",
"contentId": "null",
"lastModifiedDateTime": "datetime-value",
"id": "id-value",
"isInline": false,
"name": "example-file-2",
"size": 99 },
],
}
end

before do
allow(client_session).to receive(:graph_api_get)
.with("users/#{user_id}/messages/#{message_id}/attachments")
.and_return(graph_api_response)
end

it "returns a MailFolder for each result in the response" do
file_1 = instance_double("MicrosoftGraph::Resource::Attachment")
file_2 = instance_double("MicrosoftGraph::Resource::Attachment")

allow(MicrosoftGraph::Transformer::Attachment).to receive(:transform_collection)
.with(graph_api_response["value"], into: MicrosoftGraph::Resource::Attachment)
.and_return([file_1, file_2])

expect(client.get_file_attachments(user_id, message_id)).to match_array([file_1, file_2])
end
end
end
33 changes: 33 additions & 0 deletions spec/models/support/email_attachment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,37 @@

expect(attachment.file_size).to eq(35)
end

describe ".import_attachment" do
let(:email_attachment) { create(:support_email_attachment, outlook_id: "123") }
let(:email) { email_attachment.email }

let(:ms_attachment) do
double(id: "123", content_bytes: "1024", name: "example_file.pdf")
end

before { allow(email).to receive(:attachments).and_return(double(find_or_initialize_by: email_attachment)) }

it "imports the attachment" do
allow(email_attachment).to receive(:import_from_ms_attachment)

described_class.import_attachment(ms_attachment, email)

expect(email_attachment).to have_received(:import_from_ms_attachment).with(ms_attachment).once
end
end

describe "#import_from_ms_attachment" do
let(:email_attachment) { build(:support_email_attachment, :without_file) }
let(:ms_attachment) do
double(id: "123", content_bytes: "SGVsbG8sIFdvcmxkCg==", name: "example_file.pdf", content_type: "text/plain")
end

it "attaches file from temp file" do
email_attachment.import_from_ms_attachment(ms_attachment)

expect(email_attachment.file.download).to eq("Hello, World\n")
expect(email_attachment.file_name).to eq("example_file.pdf")
end
end
end
22 changes: 22 additions & 0 deletions spec/services/support/incoming_emails/email_attachments_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
describe Support::IncomingEmails::EmailAttachments do
let(:graph_client) { double }
let(:email) { create(:support_email) }

describe ".download" do
let(:attachment_1) { double }
let(:attachment_2) { double }

before do
allow_any_instance_of(described_class).to receive(:for_message).and_return([attachment_1, attachment_2])
end

it "converts each attachment into a Support::Attachment record" do
allow(Support::EmailAttachment).to receive(:import_attachment)

described_class.download(email: email)

expect(Support::EmailAttachment).to have_received(:import_attachment).with(attachment_1, email).once
expect(Support::EmailAttachment).to have_received(:import_attachment).with(attachment_2, email).once
end
end
end