libis-format 1.3.7.2 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45a69e173c161cfa97afd3ad4b2cc9193cc883c5acaca3708466ffec56c55d8c
4
- data.tar.gz: fad16f856808e6b02eaa31a4b02f2abba250b59b3baa821c5e1a27c17e842d98
3
+ metadata.gz: 926fc551e289a3866aa7e79ba4b835eae9ab1cae31febc247746c59a8e6bed4c
4
+ data.tar.gz: 22ab9cab0fe38e3232fbf67805ae91432ffda56b1864eaab707efa2a8d5bde86
5
5
  SHA512:
6
- metadata.gz: f9109cc481ff67637fac70eaa5e55ea107fea452e3ae1636d54e628b234e70d5531aafc6be480205bbbb7d41b7f1f1f0bf2ab467f7db816b201c59b6b733b3d9
7
- data.tar.gz: ddb206f9fa5dc4bea55859fa0660f725eab006e01fe0d039f158ca45d430bf41b3bb18591520052a996743716b400a8221a0ce73b4a2affac42ad79cc6980333
6
+ metadata.gz: bb57e0586eb783302b0b5cec06bdfec3a55f4901ed214555e602d7658a4f4908c142096145bbb0cd3573a7a56fb049a13cd9ceba85b1958efb0d70400f578a07
7
+ data.tar.gz: f30fa360afcc72db8c42713aa5b2d21a46616d8bdbe75566f29bb088cdfc3b9d11e83d1c77d22d9a9a10d00d5a383fcdddaa45386bfc187813a6aafa11cbb51b
@@ -3,6 +3,7 @@
3
3
  require_relative 'base'
4
4
 
5
5
  require 'libis/format/tool/msg_to_pdf'
6
+ require 'libis/format/tool/eml_to_pdf'
6
7
  require 'libis/format/type_database'
7
8
  require 'rexml/document'
8
9
 
@@ -10,7 +11,6 @@ module Libis
10
11
  module Format
11
12
  module Converter
12
13
  class EmailConverter < Libis::Format::Converter::Base
13
-
14
14
  def self.input_types
15
15
  %i[MSG EML]
16
16
  end
@@ -28,7 +28,22 @@ module Libis
28
28
  def convert(source, target, format, opts = {})
29
29
  super
30
30
 
31
- Format::Tool::MsgToPdf.run(source, target)
31
+ tool = tool_for_format(opts, source)
32
+ tool.run(source, target)
33
+ rescue StandardError => e
34
+ { command: { status: -1 }, errors: [{ error: e.message, error_class: e.class, error_trace: e.backtrace }] }
35
+ end
36
+
37
+ private
38
+
39
+ def tool_for_format(opts, source)
40
+ if opts[:source_format] == :MSG || File.extname(source).casecmp('.msg').zero?
41
+ Format::Tool::MsgToPdf
42
+ elsif opts[:source_format] == :EML || File.extname(source).casecmp('.eml').zero?
43
+ Format::Tool::EmlToPdf
44
+ else
45
+ raise "Unsupported file extension #{File.extname(source)}"
46
+ end
32
47
  end
33
48
  end
34
49
  end
@@ -0,0 +1,168 @@
1
+ # rubocop:disable Style/Documentation, Metrics/*
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'mail_to_pdf'
5
+
6
+ require 'mail'
7
+ require 'word_wrap'
8
+
9
+ module Libis
10
+ module Format
11
+ module Tool
12
+ class EmlToPdf < MailToPdf
13
+ protected
14
+
15
+ def open_email(source)
16
+ eml = File.read(source)
17
+ begin
18
+ msg = Mail.new(eml)
19
+ rescue StandardError => e
20
+ eml.force_encoding('ASCII-8BIT')
21
+ msg = Mail.new(eml)
22
+ @warnings << "Failed to parse message with default encoding, forced ASCII-8BIT: #{e.message}"
23
+ end
24
+ msg
25
+ rescue StandardError => e
26
+ raise "Failed to open message: #{e.message}"
27
+ end
28
+
29
+ def close_email(_msg)
30
+ true
31
+ end
32
+
33
+ def get_body_html(msg)
34
+ # Get the encoding
35
+
36
+ if !msg.multipart?
37
+ HTML_WRAPPER_TEMPLATE % WordWrap.ww(msg.decoded, 120, false)
38
+
39
+ elsif msg.html_part
40
+ body = msg.html_part.body.decoded
41
+
42
+ # if the encoding is not UTF-8, then the HTML may contain metadata that specifies the encoding
43
+ # the browser will use that metadata to render the HTML correctly, so we should not force it to UTF-8
44
+ encoding = body.scan(/<\?xml\s[^?>]*encoding="([^"]*)"[^?>]*\?>/).flatten.first
45
+ encoding ||= body.scan(/<meta\s+[^>]*charset=["']?([^"'>\s]+)["']?[^>]*>/).flatten.first
46
+ encoding ||= msg.html_part.charset
47
+ encoding ||= msg.content_type_parameters['charset'] || msg.charset || 'UTF-8'
48
+
49
+ body.force_encoding(encoding) unless encoding.casecmp(body.encoding.name).zero?
50
+
51
+ body
52
+ elsif msg.text_part
53
+ HTML_WRAPPER_TEMPLATE % msg.text_part.decoded
54
+
55
+ else
56
+ HTML_WRAPPER_TEMPLATE % ''
57
+
58
+ end
59
+ end
60
+
61
+ def get_subject(msg)
62
+ msg.subject || 'No Subject'
63
+ end
64
+
65
+ def get_headers(msg)
66
+ headers = {}
67
+ html = ''
68
+
69
+ field_list = msg.header_fields
70
+
71
+ %w[From To Cc Subject Date].each do |key|
72
+ value = find_hdr(field_list, key)
73
+ next unless value
74
+
75
+ if value.is_a? Time
76
+ begin
77
+ headers[key.downcase.to_sym] = value.iso8601
78
+ html += hdr_html(key, value.rfc2822)
79
+ rescue StandardError => e
80
+ logger.warn "Failed to parse date header '#{value}': #{e.message}"
81
+ end
82
+ else
83
+ headers[key.downcase.to_sym] = value
84
+ html += hdr_html(key, value)
85
+ end
86
+ end
87
+
88
+ [headers, html]
89
+ end
90
+
91
+ def get_inline_attachment_data(attachments, cid)
92
+ attachments.each do |attachment|
93
+ next unless attachment.has_content_id?
94
+ next unless attachment.cid == cid
95
+
96
+ begin
97
+ attachment.data.rewind
98
+ rescue NoMethodError
99
+ # do nothing, attachment.data is not a stream
100
+ end
101
+ return {
102
+ mime_type: attachment.mime_type,
103
+ base64: Base64.strict_encode64(attachment.read)
104
+ }
105
+ end
106
+ nil
107
+ end
108
+
109
+ def get_file_attachments(attachments, _used_files)
110
+ attachments.select do |attachment|
111
+ !attachment.inline? &&
112
+ !attachment.has_content_id? &&
113
+ attachment.mime_type != 'message/rfc822'
114
+ end
115
+ end
116
+
117
+ def get_mail_attachments(attachments)
118
+ attachments.select do |attachment|
119
+ !attachment.inline? &&
120
+ !attachment.has_content_id? &&
121
+ attachment.mime_type == 'message/rfc822'
122
+ end
123
+ end
124
+
125
+ def get_attachment_info(attachment)
126
+ if attachment.mime_type == 'message/rfc822'
127
+ sub_msg = Mail.new(attachment.body.to_s)
128
+ subject = sub_msg.subject.to_s.strip
129
+
130
+ {
131
+ embedded_msg: sub_msg,
132
+ filename: subject
133
+ }
134
+
135
+ elsif attachment.filename
136
+
137
+ {
138
+ data: attachment.decoded,
139
+ filename: attachment.filename
140
+ }
141
+
142
+ else
143
+ {
144
+ filename: attachment.content_id.to_s
145
+ }
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def find_hdr(list, key)
152
+ hdr = list.find { |x| x.name.to_s =~ /^#{key}$/i }
153
+ return nil unless hdr
154
+
155
+ field = hdr.field
156
+ return hdr.value unless field
157
+
158
+ return field.decoded unless field.is_a? Mail::CommonDateField
159
+
160
+ return field.date_time.to_time.localtime if field.respond_to?(:date_time)
161
+
162
+ DateTime.parse(field.decoded).to_time.localtime
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ # rubocop:enable Style/Documentation, Metrics/*
@@ -0,0 +1,447 @@
1
+ # rubocop:disable Style/Documentation, Metrics/*
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+ require 'cgi'
6
+ require 'pdfkit'
7
+ require 'time'
8
+ require 'fileutils'
9
+ require 'pathname'
10
+ require 'libis/format/config'
11
+
12
+ module Libis
13
+ module Format
14
+ module Tool
15
+ HEADER_STYLE = <<~HTML
16
+ <style>
17
+ .header-table {
18
+ margin: 0 0 10px 0;
19
+ padding: 0;
20
+ font-family: Arial, Helvetica, sans-serif;
21
+ }
22
+ .header-table table {
23
+ width: 100%;
24
+ }
25
+ .header-name {
26
+ padding-right: 5px;
27
+ color: #9E9E9E;
28
+ text-align: right;
29
+ vertical-align: top;
30
+ font-size: 12px;
31
+ }
32
+ .header-value {
33
+ font-size: 12px;
34
+ width: 99%;
35
+ }
36
+ .header_fields {
37
+ background: white;
38
+ margin: 0;
39
+ border: 1px solid #DDD;
40
+ border-radius: 3px;
41
+ padding: 8px;
42
+ box-sizing: border-box;
43
+ }
44
+ </style>
45
+ HTML
46
+
47
+ HEADER_TABLE_TEMPLATE = <<~HTML
48
+ <div class="header-table">
49
+ <table class="header_fields">
50
+ <tbody>
51
+ %s
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ HTML
56
+
57
+ HEADER_FIELD_TEMPLATE = <<~HTML
58
+ <tr>
59
+ <td class="header-name">%s</td>
60
+ <td class="header-value">%s</td>
61
+ </tr>
62
+ HTML
63
+
64
+ HTML_WRAPPER_TEMPLATE = <<~HTML
65
+ <!DOCTYPE html>
66
+ <html>
67
+ <head>
68
+ <style>
69
+ body {
70
+ font-size: 12px;
71
+ }
72
+ </style>
73
+ <title>title</title>
74
+ </head>
75
+ <body>
76
+ <pre>
77
+ %s
78
+ </pre>
79
+ </body>
80
+ </html>
81
+ HTML
82
+
83
+ HTML_BODY_TEMPLATE = <<~HTML
84
+ <!DOCTYPE html>
85
+ <html>
86
+ <head>
87
+ <style>
88
+ body {
89
+ font-size: 12px;
90
+ }
91
+ </style>
92
+ <title>title</title>
93
+ </head>
94
+ <body>
95
+ %s
96
+ </body>
97
+ </html>
98
+ HTML
99
+
100
+ ATTACHMENT_STYLE = <<~HTML
101
+ <style>
102
+ .attachment-list {
103
+ border: 1px solid #DDD;
104
+ margin: 0 0 10px 0;
105
+ padding: 0;
106
+ font-family: Arial, Helvetica, sans-serif;
107
+ }
108
+ .attachment-list ul {
109
+ list-style: disclosure-closed;
110
+ }
111
+ .attachment-list li {
112
+ font-size: 12px;
113
+ padding-left: 1em;
114
+ }
115
+ </style>
116
+ HTML
117
+
118
+ ATTACHMENT_LIST_TEMPLATE = <<~HTML
119
+ <div class="attachment-list">
120
+ <ul>
121
+ %s
122
+ </ul>
123
+ </div>
124
+ HTML
125
+
126
+ HTML_DOCTYPE_TEMPLATE = '<!DOCTYPE html>%s'
127
+ ATTACHMENT_ITEM_TEMPLATE = '<li>%s</li>'
128
+
129
+ IMG_CID_PLAIN_REGEX = /\[cid:(.*?)\]/im
130
+ IMG_CID_HTML_REGEX = /cid:([^"]*)/im
131
+
132
+ class MailToPdf
133
+ include ::Libis::Tools::Logger
134
+
135
+ def self.installed?
136
+ File.exist?(Libis::Format::Config[:wkhtmltopdf])
137
+ end
138
+
139
+ def self.run(source, target, **options)
140
+ new.run source, target, **options
141
+ end
142
+
143
+ def run(source, target, **options)
144
+ # Preliminary checks
145
+ @warnings = []
146
+
147
+ # PDF creation options
148
+ @pdf_options = {
149
+ page_size: 'A4',
150
+ margin_top: '10mm',
151
+ margin_bottom: '10mm',
152
+ margin_left: '10mm',
153
+ margin_right: '10mm',
154
+ # image_quality: 100,
155
+ # viewport_size: '2480x3508',
156
+ dpi: 300
157
+ }.merge options
158
+
159
+ # Check if source file exists
160
+ raise "File #{source} does not exist" unless File.exist?(source)
161
+
162
+ # Open the email
163
+ email = open_email(source)
164
+
165
+ # Convert the email message to PDF
166
+ result = email_to_pdf(email, target, root_msg: true)
167
+
168
+ # Close email message
169
+ close_email(email)
170
+
171
+ result
172
+ end
173
+
174
+ protected
175
+
176
+ def email_to_pdf(msg, target, root_msg: false)
177
+ # Make sure the target directory exists
178
+ outdir = File.dirname(target)
179
+ FileUtils.mkdir_p(outdir)
180
+
181
+ # Process the message body
182
+ # ------------------------
183
+ body = get_body(msg)
184
+
185
+ # Process headers
186
+ # ---------------
187
+ headers, headers_html = get_headers(msg)
188
+
189
+ # Add header section to the HTML body
190
+ body = add_headers_to_body(body, headers_html)
191
+
192
+ # Embed inline images
193
+ # -------------------
194
+ attachments = msg.attachments
195
+ used_files = embed_inline_attachments(body, attachments)
196
+
197
+ # Save other attachments
198
+ # ----------------------
199
+ attachments_dir = "#{target}.attachments"
200
+
201
+ files = save_attachments(attachments, attachments_dir, used_files)
202
+
203
+ # Add attachment section to the HTML body
204
+ body = add_attachments_to_body(body, files, attachments_dir)
205
+
206
+ # Save the HTML body as a .html file next to the PDF for debugging purposes
207
+ File.open("#{target}.html", 'wb') { |f| f.write(body) }
208
+
209
+ # Create PDF
210
+ # ----------
211
+ write_target_file(body, get_subject(msg), target)
212
+
213
+ files = [target] + files if File.exist?(target)
214
+
215
+ if root_msg
216
+ p = Pathname(File.dirname(files.first))
217
+ files.drop(1).each do |f|
218
+ (headers[:attachments] ||= []) << Pathname.new(f).relative_path_from(p).to_s
219
+ end
220
+ end
221
+
222
+ {
223
+ command: { status: 0 },
224
+ files:,
225
+ headers:,
226
+ warnings: @warnings
227
+ }
228
+ rescue StandardError => e
229
+ raise unless root_msg
230
+
231
+ close_email(msg) if msg
232
+ {
233
+ command: { status: -1 },
234
+ files: [],
235
+ headers: {},
236
+ errors: [
237
+ {
238
+ error: e.message,
239
+ error_class: e.class.name,
240
+ error_trace: e.backtrace
241
+ }
242
+ ],
243
+ warnings: @warnings
244
+ }
245
+ end
246
+
247
+ def get_body(msg)
248
+ body = get_body_html(msg)
249
+
250
+ body = HTML_BODY_TEMPLATE % body unless /<body[^>]*>/i.match?(body)
251
+ body = HTML_DOCTYPE_TEMPLATE % body unless /<!DOCTYPE html/i.match?(body)
252
+ body.sub!(%r{<title>title</title>}, "<title>#{get_subject(msg)}</title>")
253
+
254
+ body
255
+ end
256
+
257
+ def add_headers_to_body(body, headers_html)
258
+ encoding = body.encoding
259
+ return body if headers_html.empty?
260
+
261
+ b = body.downcase
262
+
263
+ # Insert header block styles
264
+ if b.include?('</head>')
265
+ # if head exists, append the style block
266
+ body.gsub!(%r{</head>}i, "#{HEADER_STYLE}</head>")
267
+ elsif b.include?('<head/>')
268
+ # empty head, replace with the style block
269
+ body.gsub!(%r{<head/>}i, "<head>#{HEADER_STYLE}</head>")
270
+ else
271
+ # otherwise insert a head section before the body tag
272
+ body.gsub!(/<body/i, "<head>#{HEADER_STYLE}</head><body")
273
+ end
274
+ # Add the headers html table as first element in the body section
275
+ body.gsub!(/<body[^>]*>/i) { |m| "#{m}#{HEADER_TABLE_TEMPLATE % headers_html.encode(encoding)}" }
276
+ body
277
+ end
278
+
279
+ def hdr_html(key, value)
280
+ if key.is_a?(String) && value.is_a?(String) && !value.empty?
281
+ return format(HEADER_FIELD_TEMPLATE, key,
282
+ CGI.escapeHTML(value))
283
+ end
284
+
285
+ ''
286
+ end
287
+
288
+ def embed_inline_attachments(body, attachments)
289
+ used_files = []
290
+
291
+ # First process plaintext cid entries
292
+ body.gsub!(IMG_CID_PLAIN_REGEX) do |_match|
293
+ data = get_inline_attachment_data(attachments, ::Regexp.last_match(1))
294
+ if data
295
+ used_files << ::Regexp.last_match(1)
296
+ "<img src=\"data:#{data[:mime_type]};base64,#{data[:base64]}\"/>"
297
+ else
298
+ '<img src=""/>'
299
+ end
300
+ end
301
+
302
+ # Then process HTML img tags with CID entries
303
+ body.gsub!(IMG_CID_HTML_REGEX) do |_match|
304
+ data = get_inline_attachment_data(attachments, ::Regexp.last_match(1))
305
+ if data
306
+ used_files << ::Regexp.last_match(1)
307
+ "data:#{data[:mime_type]};base64,#{data[:base64]}"
308
+ else
309
+ ''
310
+ end
311
+ end
312
+
313
+ used_files
314
+ end
315
+
316
+ def save_attachments(attachments, outdir, used_files)
317
+ files = []
318
+
319
+ digits = ((attachments.count + 1) / 10) + 1
320
+ i = 1
321
+
322
+ get_attachments(attachments, used_files).each do |attachment|
323
+ prefix = "#{format('%0*d', digits, i)}-"
324
+
325
+ info = get_attachment_info(attachment)
326
+
327
+ if info[:embedded_msg]
328
+ sub_msg = info[:embedded_msg]
329
+ file = File.join(outdir, "#{prefix}#{info[:filename].tr('/', '_')}.msg.pdf")
330
+
331
+ result = email_to_pdf(sub_msg, file, root_msg: false)
332
+
333
+ if (e = result[:error])
334
+ raise e
335
+ end
336
+
337
+ files += result[:files]
338
+ elsif info[:data]
339
+ file = File.join(outdir, "#{prefix}#{info[:filename].tr('/', '_')}")
340
+ FileUtils.mkdir_p(File.dirname(file))
341
+ File.open(file, 'wb') { |f| f.write(info[:data]) }
342
+ files << file
343
+ else
344
+ @warnings << "Attachment #{info[:filename]} cannot be extracted"
345
+ next
346
+ end
347
+
348
+ i += 1
349
+ end
350
+ files
351
+ end
352
+
353
+ def add_attachments_to_body(body, files, attachments_dir)
354
+ return body if files.empty?
355
+
356
+ b = body.downcase
357
+
358
+ # Insert attachment block styles
359
+ if b.include?('</head>')
360
+ # if head exists, append the style block
361
+ body.gsub!(%r{</head>}i, "#{ATTACHMENT_STYLE}</head>")
362
+ elsif b.include?('<head/>')
363
+ # empty head, replace with the style block
364
+ body.gsub!(%r{<head/>}i, "<head>#{ATTACHMENT_STYLE}</head>")
365
+ else
366
+ # otherwise insert a head section before the body tag
367
+ body.gsub!(/<body/i, "<head>#{ATTACHMENT_STYLE}</head><body")
368
+ end
369
+
370
+ # Filter files to only include those that are in the attachments directory
371
+ # and map them to relative paths from the attachments directory
372
+ items = files.filter_map do |f|
373
+ Pathname.new(f).relative_path_from(Pathname.new(attachments_dir)).to_s if File.dirname(f) == attachments_dir
374
+ end
375
+
376
+ # Create the attachment item HTML
377
+ items = items.map do |f|
378
+ format(ATTACHMENT_ITEM_TEMPLATE, f, File.basename(f))
379
+ end.join("\n")
380
+
381
+ # Create the attachment list HTML
382
+ attachments_html = ATTACHMENT_LIST_TEMPLATE % items
383
+
384
+ # make sure the attachments_html is encoded in the same encoding as the body
385
+ attachments_html = attachments_html.encode(body.encoding)
386
+
387
+ # Add the attachments html list after the headers
388
+ # if there are no headers, then add it at the beginning of the body section
389
+ body.sub!(%r{<div class="header-table">.*?</div>}im) { |m| "#{m}#{attachments_html}" } ||
390
+ body.gsub!(/<body[^>]*>/i) { |m| "#{m}#{attachments_html}" }
391
+
392
+ body
393
+ end
394
+
395
+ def get_attachments(attachments, used_files)
396
+ get_file_attachments(attachments, used_files) + get_mail_attachments(attachments)
397
+ end
398
+
399
+ def write_target_file(body, title, target)
400
+ kit = PDFKit.new(body, title: title || 'message', **@pdf_options)
401
+ pdf = kit.to_pdf
402
+ File.open(target, 'wb') { |f| f.write(pdf) }
403
+ end
404
+
405
+ # ---------------------------------------
406
+ # Methods to be implemented by subclasses
407
+ # ---------------------------------------
408
+ def open_email(_source)
409
+ raise NotImplementedError, 'Subclasses must implement the open_email method'
410
+ end
411
+
412
+ def close_email(_email)
413
+ raise NotImplementedError, 'Subclasses must implement the close_email method'
414
+ end
415
+
416
+ def get_body_html(_msg)
417
+ raise NotImplementedError, 'Subclasses must implement the get_body_html method'
418
+ end
419
+
420
+ def get_subject(_msg)
421
+ raise NotImplementedError, 'Subclasses must implement the get_subject method'
422
+ end
423
+
424
+ def get_headers(_msg)
425
+ raise NotImplementedError, 'Subclasses must implement the get_headers method'
426
+ end
427
+
428
+ def get_inline_attachment_data(_attachments, _cid)
429
+ raise NotImplementedError, 'Subclasses must implement the get_inline_attachment_data method'
430
+ end
431
+
432
+ def get_file_attachments(_attachments, _used_files)
433
+ raise NotImplementedError, 'Subclasses must implement the get_file_attachments method'
434
+ end
435
+
436
+ def get_mail_attachments(_attachments)
437
+ raise NotImplementedError, 'Subclasses must implement the get_mail_attachments method'
438
+ end
439
+
440
+ def get_attachment_info(_attachment)
441
+ raise NotImplementedError, 'Subclasses must implement the get_attachment_info method'
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end
447
+ # rubocop:enable Style/Documentation, Metrics/*
@@ -1,68 +1,39 @@
1
+ # rubocop:disable Style/Documentation, Metrics/*
1
2
  # frozen_string_literal: true
2
3
 
4
+ require_relative 'mail_to_pdf'
5
+
3
6
  require 'mapi/msg'
4
7
  require 'rfc_2047'
5
- require 'cgi'
6
- require 'pdfkit'
7
- require 'time'
8
- require 'fileutils'
9
- require 'pathname'
10
- require 'libis/format/config'
8
+
9
+ Mapi::Log.level = Logger::Severity::INFO
11
10
 
12
11
  module Libis
13
12
  module Format
14
13
  module Tool
15
- class MsgToPdf
16
- include ::Libis::Tools::Logger
17
-
18
- HEADER_STYLE = '<style>.header-table {margin: 0 0 20 0;padding: 0;font-family: Arial, Helvetica, sans-serif;}.header-name {padding-right: 5px;color: #9E9E9E;text-align: right;vertical-align: top;font-size: 12px;}.header-value {font-size: 12px;}#header_fields {#background: white;#margin: 0;#border: 1px solid #DDD;#border-radius: 3px;#padding: 8px;#width: 100%%;#box-sizing: border-box;#}</style><script type="text/javascript">function timer() {try {parent.postMessage(Math.max(document.body.offsetHeight, document.body.scrollHeight), \'*\');} catch (r) {}setTimeout(timer, 10);};timer();</script>' # rubocop:disable Layout/LineLength
19
- HEADER_TABLE_TEMPLATE = '<div class="header-table"><table id="header_fields"><tbody>%s</tbody></table></div>'
20
- HEADER_FIELD_TEMPLATE = '<tr><td class="header-name">%s</td><td class="header-value">%s</td></tr>'
21
- HTML_WRAPPER_TEMPLATE = '<!DOCTYPE html><html><head><style>body {font-size: 0.5cm;}</style><title>title</title></head><body>%s</body></html>' # rubocop:disable Layout/LineLength
22
-
23
- IMG_CID_PLAIN_REGEX = /\[cid:(.*?)\]/m
24
- IMG_CID_HTML_REGEX = /cid:([^"]*)/m
25
-
26
- def self.installed?
27
- File.exist?(Libis::Format::Config[:wkhtmltopdf])
28
- end
29
-
30
- def self.run(source, target, **options)
31
- new.run source, target, **options
32
- end
33
-
34
- def run(source, target, **options)
35
- # Preliminary checks
36
- # ------------------
37
-
38
- @warnings = []
39
-
40
- # Check if source file exists
41
- raise "File #{source} does not exist" unless File.exist?(source)
42
-
43
- # Retrieving the message
44
- # ----------------------
14
+ class MsgToPdf < MailToPdf
15
+ protected
45
16
 
17
+ def open_email(source)
46
18
  # Open the message
47
- msg = Mapi::Msg.open(source)
19
+ Mapi::Msg.open(source)
20
+ end
48
21
 
49
- target_format = options.delete(:to_html) ? :HTML : :PDF
50
- result = msg_to_pdf(msg, target, target_format, options)
22
+ def close_email(msg)
51
23
  msg.close
52
- result
24
+ true
53
25
  end
54
26
 
55
- def msg_to_pdf(msg, target, target_format, pdf_options, root_msg: true)
56
- # Make sure the target directory exists
57
- outdir = File.dirname(target)
58
- FileUtils.mkdir_p(outdir)
59
-
27
+ def get_body_html(msg)
60
28
  # Get the body of the message in HTML
61
29
  body = msg.properties.body_html
62
30
 
63
31
  # Embed plain body in HTML as a fallback
64
32
  body ||= HTML_WRAPPER_TEMPLATE % msg.properties.body
65
33
 
34
+ # Worst case, just create empty body
35
+ body ||= HTML_WRAPPER_TEMPLATE % ''
36
+
66
37
  # Check and fix the character encoding
67
38
  begin
68
39
  # Try to encode into UTF-8
@@ -78,161 +49,92 @@ module Libis
78
49
  end
79
50
  end
80
51
 
81
- # Process headers
82
- # ---------------
52
+ body
53
+ end
54
+
55
+ def get_subject(msg)
56
+ find_hdr(msg.headers, 'Subject') || 'No Subject'
57
+ end
58
+
59
+ def get_headers(msg)
83
60
  headers = {}
84
- hdr_html = ''
61
+ html = ''
85
62
 
86
63
  %w[From To Cc Subject Date].each do |key|
87
64
  value = find_hdr(msg.headers, key)
88
- if value
65
+ next unless value
66
+
67
+ if key.casecmp('Date').zero?
68
+ begin
69
+ value = DateTime.parse(value).to_time.localtime
70
+ headers[key.downcase.to_sym] = value.iso8601
71
+ html += hdr_html(key, value.rfc2822)
72
+ rescue StandardError => e
73
+ logger.warn "Failed to parse date header '#{value}': #{e.message}"
74
+ end
75
+ else
89
76
  headers[key.downcase.to_sym] = value
90
- hdr_html += hdr_html(key, value)
77
+ html += hdr_html(key, value)
91
78
  end
92
79
  end
93
80
 
94
- [:date].each do |key|
95
- next unless headers[key]
81
+ [headers, html]
82
+ end
96
83
 
97
- headers[key] = DateTime.parse(headers[key]).to_time.localtime.iso8601
98
- end
84
+ def get_inline_attachment_data(attachments, cid)
85
+ attachments.each do |attachment|
86
+ next unless attachment.properties.attach_content_id == cid
99
87
 
100
- # Add header section to the HTML body
101
- unless hdr_html.empty?
102
- # Insert header block styles
103
- if body =~ %r{</head>}
104
- # if head exists, append the style block
105
- body.gsub!(%r{</head>}, "#{HEADER_STYLE}</head>")
106
- elsif body =~ %r{<head/>}
107
- # empty head, replace with the style block
108
- body.gsub!(%r{<head/>}, "<head>#{HEADER_STYLE}</head>")
109
- else
110
- # otherwise insert a head section before the body tag
111
- body.gsub!(/<body/, "<head>#{HEADER_STYLE}</head><body")
112
- end
113
- # Add the headers html table as first element in the body section
114
- body.gsub!(/<body[^>]*>/) { |m| "#{m}#{HEADER_TABLE_TEMPLATE % hdr_html}" }
88
+ attachment.data.rewind
89
+ return {
90
+ mime_type: attachment.properties.attach_mime_tag,
91
+ base64: Base64.encode64(attachment.data.read).gsub(/[\r\n]/, '')
92
+ }
115
93
  end
94
+ nil
95
+ end
116
96
 
117
- # Embed inline images
118
- # -------------------
119
- attachments = msg.attachments
120
- used_files = []
121
-
122
- # First process plaintext cid entries
123
- body.gsub!(IMG_CID_PLAIN_REGEX) do |_match|
124
- data = get_attachment_data(attachments, ::Regexp.last_match(1))
125
- if data
126
- used_files << ::Regexp.last_match(1)
127
- "<img src=\"data:#{data[:mime_type]};base64,#{data[:base64]}\"/>"
128
- else
129
- '<img src=""/>'
130
- end
97
+ def get_file_attachments(attachments, used_files)
98
+ attachments.select do |attachment|
99
+ !attachment.properties.attachment_hidden &&
100
+ attachment.filename &&
101
+ !attachment.filename.empty? &&
102
+ !used_files.include?(attachment.filename)
131
103
  end
104
+ end
132
105
 
133
- # Then process HTML img tags with CID entries
134
- body.gsub!(IMG_CID_HTML_REGEX) do |_match|
135
- data = get_attachment_data(attachments, ::Regexp.last_match(1))
136
- if data
137
- used_files << ::Regexp.last_match(1)
138
- "data:#{data[:mime_type]};base64,#{data[:base64]}"
139
- else
140
- ''
141
- end
106
+ def get_mail_attachments(attachments)
107
+ attachments.select do |attachment|
108
+ !attachment.properties.attachment_hidden &&
109
+ attachment.instance_variable_get(:@embedded_msg)
142
110
  end
111
+ end
143
112
 
144
- # Create PDF
145
- # ----------
146
- files = []
147
-
148
- if target_format == :PDF
149
- # PDF creation options
150
- pdf_options = {
151
- page_size: 'A4',
152
- margin_top: '10mm',
153
- margin_bottom: '10mm',
154
- margin_left: '10mm',
155
- margin_right: '10mm',
156
- # image_quality: 100,
157
- # viewport_size: '2480x3508',
158
- dpi: 300
159
- }.merge pdf_options
160
-
161
- subject = find_hdr(msg.headers, 'Subject')
162
- kit = PDFKit.new(body, title: (subject || 'message'), **pdf_options)
163
- pdf = kit.to_pdf
164
- File.open(target, 'wb') { |f| f.write(pdf) }
165
- else
166
- File.open(target, 'wb') { |f| f.write(body) }
167
- end
168
- files << target if File.exist?(target)
113
+ def get_attachment_info(attachment)
114
+ if (sub_msg = attachment.instance_variable_get(:@embedded_msg))
169
115
 
170
- # Save attachments
171
- # ----------------
172
- outdir = File.join(outdir, "#{File.basename(target)}.attachments")
173
- digits = ((attachments.count + 1) / 10) + 1
174
- i = 1
175
- attachments.delete_if { |a| a.properties.attachment_hidden }.each do |a|
176
- prefix = "#{format('%0*d', digits, i)}-"
177
- if (sub_msg = a.instance_variable_get(:@embedded_msg))
178
- subject = a.properties[:display_name] || sub_msg.subject || ''
179
- file = File.join(outdir, "#{prefix}#{subject.gsub('/', '_')}.msg.#{target_format.to_s.downcase}")
180
- result = msg_to_pdf(sub_msg, file, target_format, pdf_options, root_msg: false)
181
- if (e = result[:error])
182
- raise e
183
- end
116
+ {
117
+ embedded_msg: sub_msg,
118
+ filename: attachment.properties[:display_name] || sub_msg.subject || ''
119
+ }
184
120
 
185
- files += result[:files]
186
- elsif a.filename
187
- next if used_files.include?(a.filename)
121
+ elsif attachment.filename
122
+ io = StringIO.new
123
+ attachment.save(io)
124
+ io.rewind
188
125
 
189
- file = File.join(outdir, "#{prefix}#{a.filename.gsub('/', '_')}")
190
- FileUtils.mkdir_p(File.dirname(file))
191
- File.open(file, 'wb') { |f| a.save(f) }
192
- files << file
193
- else
194
- @warnings << "Attachment #{a.properties[:display_name]} cannot be extracted"
195
- next
196
- end
197
- i += 1
198
- end
126
+ {
127
+ data: io.string,
128
+ filename: attachment.filename
129
+ }
199
130
 
200
- if root_msg
201
- p = Pathname(File.dirname(files.first))
202
- files[1..].each do |f|
203
- (headers[:attachments] ||= []) << Pathname.new(f).relative_path_from(p).to_s
204
- end
131
+ else
132
+ {
133
+ filename: attachment.properties[:display_name] || 'unknown'
134
+ }
205
135
  end
206
-
207
- {
208
- command: { status: 0 },
209
- files:,
210
- headers:,
211
- warnings: @warnings
212
- }
213
- rescue Exception => e
214
- raise unless root_msg
215
-
216
- msg.close
217
- {
218
- command: { status: -1 },
219
- files: [],
220
- headers: {},
221
- errors: [
222
- {
223
- error: e.message,
224
- error_class: e.class.name,
225
- error_trace: e.backtrace
226
- }
227
- ],
228
- warnings: @warnings
229
- }
230
136
  end
231
137
 
232
- protected
233
-
234
- def eml_to_html; end
235
-
236
138
  private
237
139
 
238
140
  def find_hdr(list, key)
@@ -245,26 +147,8 @@ module Libis
245
147
  end
246
148
  nil
247
149
  end
248
-
249
- def hdr_html(key, value)
250
- return format(HEADER_FIELD_TEMPLATE, key, CGI.escapeHTML(value)) if key.is_a?(String) && value.is_a?(String) && !value.empty?
251
-
252
- ''
253
- end
254
-
255
- def get_attachment_data(attachments, cid)
256
- attachments.each do |attachment|
257
- next unless attachment.properties.attach_content_id == cid
258
-
259
- attachment.data.rewind
260
- return {
261
- mime_type: attachment.properties.attach_mime_tag,
262
- base64: Base64.encode64(attachment.data.read).gsub(/[\r\n]/, '')
263
- }
264
- end
265
- nil
266
- end
267
150
  end
268
151
  end
269
152
  end
270
153
  end
154
+ # rubocop:enable Style/Documentation, Metrics/*
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Libis
4
4
  module Format
5
- VERSION = '1.3.7.2'
5
+ VERSION = '1.3.8'
6
6
  end
7
7
  end
data/libis-format.gemspec CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.homepage = ''
17
17
  spec.license = 'MIT'
18
18
 
19
- spec.platform = Gem::Platform::JAVA if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
19
+ # spec.platform = Gem::Platform::JAVA if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
20
20
  spec.required_ruby_version = '>= 3.2'
21
21
 
22
22
  spec.files = `git ls-files -z`.split("\x0").select do |f|
@@ -29,12 +29,15 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency 'deep_dive', '~> 0.3'
30
30
  spec.add_runtime_dependency 'libis-mapi', '~> 0.3'
31
31
  spec.add_runtime_dependency 'libis-tools', '~> 1.1'
32
+ spec.add_runtime_dependency 'mail', '~> 2.9'
32
33
  spec.add_runtime_dependency 'mini_magick', '~> 5.0.1'
33
34
  spec.add_runtime_dependency 'naturally', '~> 2.2'
34
35
  spec.add_runtime_dependency 'new_rfc_2047', '~> 1.0'
35
36
  spec.add_runtime_dependency 'os', '~> 1.1'
36
37
  spec.add_runtime_dependency 'pdfinfo', '~> 1.4'
37
38
  spec.add_runtime_dependency 'pdfkit', '~> 0.8'
39
+ # spec.add_runtime_dependency 'ruby-msg', '~> 1.5.3.1'
40
+ spec.add_runtime_dependency 'word_wrap', '~> 1.0'
38
41
 
39
42
  spec.add_development_dependency 'awesome_print'
40
43
  spec.add_development_dependency 'equivalent-xml'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libis-format
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.7.2
4
+ version: 1.3.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kris Dekeyser
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-06-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: chromaprint
@@ -66,6 +65,20 @@ dependencies:
66
65
  - - "~>"
67
66
  - !ruby/object:Gem::Version
68
67
  version: '1.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: mail
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.9'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.9'
69
82
  - !ruby/object:Gem::Dependency
70
83
  name: mini_magick
71
84
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +163,20 @@ dependencies:
150
163
  - - "~>"
151
164
  - !ruby/object:Gem::Version
152
165
  version: '0.8'
166
+ - !ruby/object:Gem::Dependency
167
+ name: word_wrap
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '1.0'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '1.0'
153
180
  - !ruby/object:Gem::Dependency
154
181
  name: awesome_print
155
182
  requirement: !ruby/object:Gem::Requirement
@@ -273,12 +300,14 @@ files:
273
300
  - lib/libis/format/identifier.rb
274
301
  - lib/libis/format/tool.rb
275
302
  - lib/libis/format/tool/droid.rb
303
+ - lib/libis/format/tool/eml_to_pdf.rb
276
304
  - lib/libis/format/tool/extension_identification.rb
277
305
  - lib/libis/format/tool/ff_mpeg.rb
278
306
  - lib/libis/format/tool/fido.rb
279
307
  - lib/libis/format/tool/file_tool.rb
280
308
  - lib/libis/format/tool/fop_pdf.rb
281
309
  - lib/libis/format/tool/identification_tool.rb
310
+ - lib/libis/format/tool/mail_to_pdf.rb
282
311
  - lib/libis/format/tool/msg_to_pdf.rb
283
312
  - lib/libis/format/tool/office_to_pdf.rb
284
313
  - lib/libis/format/tool/pdf_merge.rb
@@ -352,7 +381,6 @@ homepage: ''
352
381
  licenses:
353
382
  - MIT
354
383
  metadata: {}
355
- post_install_message:
356
384
  rdoc_options: []
357
385
  require_paths:
358
386
  - lib
@@ -367,8 +395,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
367
395
  - !ruby/object:Gem::Version
368
396
  version: '0'
369
397
  requirements: []
370
- rubygems_version: 3.4.19
371
- signing_key:
398
+ rubygems_version: 4.0.8
372
399
  specification_version: 4
373
400
  summary: LIBIS File format format services.
374
401
  test_files: []