postmortem 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0231d0fd15050a1535451df10e0a700f2218345be118d911c495285c35548c3
4
- data.tar.gz: 5427f04480ae4b3d59fc169949429ab17e8451cff4a7b008b84c5457766fde6b
3
+ metadata.gz: ba1b63c74d80acb294e4bf377084882db2dd7f3c5d6422079923830b43405de2
4
+ data.tar.gz: 7b359315a60e5c44d1ea0efbf1f049c04d7cf3258b7db5bcb53636ceacbfbe13
5
5
  SHA512:
6
- metadata.gz: e7568bba76a5439455684d7691216eb01e3e18165693ef649d85e981ed4cb88ab9fcfbb34abcc0d801e2080dcbd6ec4b6e10534221998c227a821e01c33d0593
7
- data.tar.gz: 7f7467751d3d4975d908e322f58c2cf1c0948c213ef6a01e28c5071db52f682fcec41c515fe21067cae95cd4d4924e1dac5682e069a5d4f734d0abb9db0707d9
6
+ metadata.gz: 57b6d1d1c02fe3f4445dfc038ef3d0762d700fca755d63a20827000d5805484a2f293f4b164bac0e22d341f24648129aa55e80f318db56243cdd32dd79dc2286
7
+ data.tar.gz: '02768d56a8cf860d2328af1922354edaf7a7caff7d481a080b27cfd3102f215084692d376f260c725a15cbdbbe0e5ec4bce2c20c0517091ad5c59495645bab75'
data/.gitignore CHANGED
@@ -10,4 +10,6 @@
10
10
  .rspec_status
11
11
  *.gem
12
12
 
13
+ preview/
14
+
13
15
  Gemfile.lock
data/.rubocop.yml CHANGED
@@ -3,49 +3,7 @@ Metrics/BlockLength:
3
3
  - 'spec/**/*_spec.rb'
4
4
  - 'postmortem.gemspec'
5
5
 
6
- Layout/EmptyLinesAroundAttributeAccessor:
7
- Enabled: true
8
- Layout/SpaceAroundMethodCallOperator:
9
- Enabled: true
10
- Lint/DeprecatedOpenSSLConstant:
11
- Enabled: true
12
- Lint/DuplicateElsifCondition:
13
- Enabled: true
14
- Lint/MixedRegexpCaptureTypes:
15
- Enabled: true
16
- Lint/RaiseException:
17
- Enabled: true
18
- Lint/StructNewOverride:
19
- Enabled: true
20
- Style/AccessorGrouping:
21
- Enabled: true
22
- Style/ArrayCoercion:
23
- Enabled: true
24
- Style/BisectedAttrAccessor:
25
- Enabled: true
26
- Style/CaseLikeIf:
27
- Enabled: true
28
- Style/ExponentialNotation:
29
- Enabled: true
30
- Style/HashAsLastArrayItem:
31
- Enabled: true
32
- Style/HashEachMethods:
33
- Enabled: true
34
- Style/HashLikeCase:
35
- Enabled: true
36
- Style/HashTransformKeys:
37
- Enabled: true
38
- Style/HashTransformValues:
39
- Enabled: true
40
- Style/RedundantAssignment:
41
- Enabled: true
42
- Style/RedundantFetchBlock:
43
- Enabled: true
44
- Style/RedundantFileExtensionInRequire:
45
- Enabled: true
46
- Style/RedundantRegexpCharacterClass:
47
- Enabled: true
48
- Style/RedundantRegexpEscape:
49
- Enabled: true
50
- Style/SlicingWithRange:
51
- Enabled: true
6
+ AllCops:
7
+ NewCops: enable
8
+ Exclude:
9
+ - 'preview/**/*'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.5.3
1
+ 2.5.8
data/Gemfile CHANGED
@@ -3,5 +3,3 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
-
7
- gem 'devpack', '~> 0.2.0'
data/README.md CHANGED
@@ -1,21 +1,24 @@
1
- # Postmortem
1
+ # PostMortem
2
2
 
3
- _Postmortem_ provides a simple and clean preview of all outgoing mails sent by your _Ruby_ application to make email development a little less painful.
3
+ _PostMortem_ provides a simple and clean preview of all outgoing mails sent by your _Ruby_ application to make email development a little less painful.
4
4
 
5
5
  Every time your application sends an email a clearly-visible log entry will be written which provides a path to a temporary file containing your preview.
6
6
 
7
- Take a look at a [live example](https://postmortem.surge.sh/) to see _Postmortem_ in action.
7
+ Take a look at a [live example](https://postmortem.surge.sh/) to see _PostMortem_ in action.
8
8
 
9
- _Postmortem_ should only be enabled in test or development environments.
9
+ _PostMortem_ should only be enabled in test or development environments.
10
10
 
11
11
  ## Features
12
12
 
13
13
  * Seamless integration with [_ActionMailer_](https://guides.rubyonrails.org/action_mailer_basics.html), [_Pony_](https://github.com/benprew/pony), [_Mail_](https://github.com/mikel/mail), etc.
14
14
  * Email deliveries are always intercepted (can be configured to pass through).
15
+ * Live inbox monitors incoming emails so you can view them as soon as they are delivered.
15
16
  * Preview email content as well as typical email headers (recipients, subject, etc.).
16
17
  * View rendered _HTML_, plaintext, or _HTML_ source with syntax highlighting (courtesy of [highlight.js](https://highlightjs.org/)).
17
- * Dual or single column view to suit your requirements.
18
18
  * Content is loaded inside an `<iframe>` to ensure document isolation and validity.
19
+ * Local images are located and embedded in HTML so you can see the full version of outgoing emails.
20
+ * Runs without a server - single page app runs on file system with no need to run a local web server to access UI.
21
+ * Any captured email can be downloaded into a standalone HTML file which can be shared with others.
19
22
 
20
23
  ## Installation
21
24
 
@@ -23,7 +26,7 @@ Add the gem to your application's Gemfile:
23
26
 
24
27
  ```ruby
25
28
  group :development, :test do
26
- gem 'postmortem', '~> 0.2.3'
29
+ gem 'postmortem', '~> 0.2.4'
27
30
  end
28
31
  ```
29
32
 
@@ -37,9 +40,7 @@ Or install it yourself as:
37
40
 
38
41
  ## Usage
39
42
 
40
- _Postmortem_ automatically integrates with _Rails ActionMailer_ and _Pony_. When an email is sent an entry will be visible in your application's log output.
41
-
42
- The path to the preview file is based on the current time and the subject of the email. If you would prefer to use the same path for each email you can disable timestamps (see [configuration](#configuration)) and simply reload your browser every time an email is sent.
43
+ _PostMortem_ automatically integrates with _Rails ActionMailer_ and _Pony_. When an email is sent an entry will be visible in your application's log output.
43
44
 
44
45
  If you are using assets (images etc.) with _ActionMailer_ make sure to configure the asset host, e.g.:
45
46
 
@@ -50,15 +51,22 @@ Rails.application.configure do
50
51
  end
51
52
  ```
52
53
 
53
- Load the provided file in your browser to preview your email.
54
+ A log entry will be generated every time an email is sent. Load the path provided in the log entry in your browser to launch _PostMortem_:
54
55
 
55
56
  ![Screenshot](doc/screenshot.png)
56
57
 
58
+ ### Clearing the inbox
59
+
60
+ The inbox can be cleared at any time (e.g. at the start of a test run):
61
+
62
+ ```ruby
63
+ Postmortem.clear_inbox
64
+ ```
57
65
 
58
66
  ## Configuration
59
67
  <a name="configuration"></a>
60
68
 
61
- Configure _Postmortem_ by calling `Postmortem.configure`, e.g. in a _Rails_ initializer.
69
+ Configure _PostMortem_ by calling `Postmortem.configure`, e.g. in a _Rails_ initializer.
62
70
 
63
71
  ```ruby
64
72
  # config/initializers/postmortem.rb
@@ -66,7 +74,7 @@ Postmortem.configure do |config|
66
74
  # Colorize output in logs (path to preview HTML file) to improve visibility (default: true).
67
75
  config.colorize = true
68
76
 
69
- # Path to the Postmortem log file, where preview paths are written (default: STDOUT).
77
+ # Path to the PostMortem log file, where preview paths are written (default: STDOUT).
70
78
  config.log_path = '/path/to/postmortem.log'
71
79
 
72
80
  # Path to save preview .html files (default: OS-provided temp directory).
data/doc/screenshot.png CHANGED
Binary file
@@ -1,11 +1,15 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
+ <title>PostMortem Email</title>
3
4
  <head>
4
5
  <style>
5
6
  <%= css_dependencies %>
6
7
  <%= styles %>
7
8
  </style>
9
+
10
+ <link href="data:image/x-icon;base64,<%= favicon_b64 %>" rel="icon" type="image/x-icon" />
8
11
  </head>
12
+
9
13
  <body>
10
14
  <div class="toolbar">
11
15
 
@@ -19,11 +23,11 @@
19
23
 
20
24
  <i data-toggle="tooltip"
21
25
  title="View HTML Source"
22
- class="fa fa-file-code-o source-view-switch"></i>
26
+ class="fa fa-code source-view-switch"></i>
23
27
 
24
28
  <i data-toggle="tooltip"
25
29
  title="View HTML Part"
26
- class="fa fa-code html-view-switch"></i>
30
+ class="fa fa-file-code-o html-view-switch"></i>
27
31
 
28
32
  <i data-toggle="tooltip"
29
33
  title="View Plaintext Part"
@@ -40,10 +44,26 @@
40
44
  class="fa fa-envelope-open-o headers-view-switch"></i>
41
45
 
42
46
  </div>
47
+
43
48
  <div class="content">
44
49
  <div class="container full-width">
45
50
  <div class="row main-row">
46
- <div id="inbox" class="col inbox">
51
+ <div id="inbox-container" class="col inbox-container">
52
+ <div class="row">
53
+ <div class="col inbox-header">
54
+ <h5><i class="fa fa-inbox"></i> Inbox <span class="text-secondary" id="inbox-info"></span></h5>
55
+ <button data-toggle="tooltip" title="Hide read messages" class="btn btn-light show-hide-read-button"><i class="fa fa-envelope-open-o"></i> <i data-state="show" class="fa fa-eye show-hide-read-icon"></i></button>
56
+ <button data-toggle="tooltip" title="Mark all as read" class="btn btn-light read-all-button"><i class="fa fa-envelope-open-o"></i> All</button>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="row">
61
+ <!--INBOX-START-->
62
+ <div id="inbox" class="col inbox">
63
+ <div class="inbox-loading">Loading &mdash; <i class="fa fa-clock-o"></i></div>
64
+ </div>
65
+ <!--INBOX-END-->
66
+ </div>
47
67
  </div>
48
68
 
49
69
  <div class="col right-column">
@@ -52,9 +72,11 @@
52
72
  <div class="preview row html-view">
53
73
  <iframe id="html-iframe"></iframe>
54
74
  </div>
75
+
55
76
  <div class="preview row text-view">
56
77
  <iframe id="text-iframe"></iframe>
57
78
  </div>
79
+
58
80
  <div class="preview row source-view">
59
81
  <iframe id="source-iframe"></iframe>
60
82
  </div>
@@ -68,10 +90,14 @@
68
90
  <%= javascript_dependencies %>
69
91
  </script>
70
92
  <%= headers_template %>
93
+ <script id="initialize-script" type="text/javascript">
94
+ const POSTMORTEM = {};
95
+ POSTMORTEM.downloadedPreview = false;
96
+ POSTMORTEM.initialData = <%= mail.serializable.to_json %>;
97
+ POSTMORTEM.hasHtml = <%= (!mail.html_body.nil?).to_json %>;
98
+ POSTMORTEM.hasText = <%= (!mail.text_body.nil?).to_json %>;
99
+ </script>
71
100
  <script>
72
- var initialData = <%= mail.serializable.to_json %>;
73
- var hasHtml = <%= (!mail.html_body.nil?).to_json %>;
74
- var hasText = <%= (!mail.text_body.nil?).to_json %>;
75
101
  <%= javascript %>
76
102
  </script>
77
103
  </body>
@@ -0,0 +1 @@
1
+ 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zI3d1cyN3dXMjd3VzI3d1cyN3dXMjd3VzI3d1cyN3dXMjd3VzI3d1cyN3dXMjd7XzIvd2M1h4+PGCQAAAAC4raMZtqub2NDHu//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9DHu/+2q5vYuK2jGc/Iu6a3rJv/tqua/9DHu//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Qx7v/tqua/7esm//PyLum3NbM89PLv/+3rJv/tqua/9DHu//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/0Me7/7armv+3rJv/08u//9zWzPPd1sz/3dbM/9PLv/+3rJv/tqua/9DHu//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/c1Mr/3NXK/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9DHu/+2q5r/t6yb/9PLv//d1sz/3dbM/93WzP/d1sz/3dbM/9PLv/+3rJv/tqua/9DHuv/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Ryb7/vbOj/7Wqmf+1qpn/vrSk/9PLv//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Qx7v/tqua/7esm//Ty7//3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9PLv/+3rJv/tqua/9DHu//d1sz/3dbM/93WzP/d1sz/zcS3/7Wqmf+1qpn/taqZ/7Wqmf+1qpn/taqa/87Guf/d1sz/3dbM/93WzP/d1sz/0Mi7/7armv+3rJv/08u//93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9PLv/+3rJv/tqua/9DHu//d1sz/3dbM/83Et/+1qpn/taqZ/8S7rf/WzsP/1s7D/8S7rf+1qpn/taqZ/87Guf/d1sz/3dbM/9DHu/+2q5r/t6yb/9PLv//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9PLv/+3rJv/tqua/9DHuv/NxLf/taqZ/7Wqmf/WzsP/7ejh/+3o4f/t6OH/7ejh/9bOw/+1qpn/taqZ/87Guf/Qx7v/tqua/7esm//Ty7//3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9PLv/+3rJv/taqa/7Wqmf+1qpn/18/E/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9fPxP+1qpn/taqZ/7armv+3rJv/08u//93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/8a9r/+1qpn/taqZ/9fPxP/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9fPxP+1qpn/taqZ/8e+sP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/MxLf/taqZ/7Wqmf/Xz8T/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9fPxP+1qpn/taqZ/87Fuf/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/zMS3/7Wqmf+1qpn/1s/E/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83FuP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/8zEt/+1qpn/taqZ/9bPxP/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83FuP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/MxLf/taqZ/7Wqmf/Wz8T/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83FuP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/zMS3/7Wqmf+1qpn/1s/E/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83FuP/d1sz/3dbM/93WzP/d1sz/3dbM/8zEt/+1qpn/taqZ/9bPxP/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83Ft//d1sz/3dbM/93WzP/MxLf/taqZ/7Wqmf/Wz8P/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83Et//d1sz/zMS3/7Wqmf+1qpn/1s7D/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/83Et/+1qpn/taqZ/9bPw//t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1qpn/taqZ/7WrmvXWzsP/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9bPxP+1q5r12dLJqe3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9vVyqnr4tga7Ofg2e3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/s5+DZ6+LYGgAAAADR0dEL6+beZezm35Dq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPq5d6T6uXek+rl3pPs5t+Q6+beZdHR0QsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAADAAAABgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8zMBdzTyh3g1swy3NTMQdzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzVzULc1c1C3NXNQtzUzEHg1swy3NPKHf/MzAUAAAAAAAAAAAAAAAC6sZ0avbOjgNrSx9Pd1sz53dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz52tLH072xo4C6sZ0aAAAAALa2pA63rJuctqqZ+sG3qf/a0sj/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/a0sj/wbep/7aqmfq3rJuctrakDs3GulG9saL6taqZ/7Wqmv/Euqz/2tLI/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9rSyP/Euqz/taqa/7Wqmf+9saL6zca6Ud3Vy7zTy7//vLGh/7Wqmf+2q5r/xLqs/9rSyP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/2tLI/8S6rP+2q5r/taqZ/7yxof/Ty7//3dXLvN3WzPXd1sz/1c7D/7qwn/+1qpn/taqa/8G3qP/a0sj/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/a0sj/wbep/7Wqmv+1qpn/urCf/9XOw//d1sz/3dbM9d3WzP/d1sz/3dbM/9XOw/+8saH/taqZ/7Wqmf/Euqz/2tLI/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3NXL/9zVy//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9rSyP/Euqz/taqa/7Wqmf+8saH/1c7D/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Ty7//vLGh/7Wqmf+2q5r/xLqs/9rSx//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3NXL/9PMwP/LwrX/x76w/8e+sP/Mw7b/1M3B/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/2tLI/8S6rP+2q5r/taqZ/7yxof/Ty7//3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/1c7D/7qwn/+1qpn/taqZ/8G3qP/a0sf/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Z0sf/w7qs/7iunf+2q5r/taqZ/7Wqmf+2q5r/ua6e/8W8rv/a08j/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/a0sj/wbep/7Wqmv+1qpn/urCf/9XOw//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9XOw/+8saH/taqZ/7Wqmf/Euqz/2tLI/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9jRxv/Bt6j/taqZ/7Wqmf+1qpn/taqZ/7Wqmf+1qpn/taqZ/7Wqmf/Cuar/2dLH/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9rTyP/Euqz/taqa/7Wqmf+8saH/1c7D/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Ty7//vLGh/7Wqmf+2q5r/xLqs/9rSyP/d1sz/3dbM/93WzP/d1sz/2NDF/8G3qP+1qpn/taqZ/7Wqmf+2rJv/u7Gh/7uxof+2q5v/taqZ/7Wqmf+1qpn/wrmq/9nRx//d1sz/3dbM/93WzP/d1sz/2tPI/8S6rP+2q5r/taqZ/7yxof/Ty7//3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/1s7D/7qwn/+1qpn/taqZ/8G3qP/a0sf/3dbM/93WzP/Y0MX/vrSl/7Wqmf+1qpn/taqZ/8i/sf/f2M//5uDY/+bg1//f2M//yL+x/7Wqmf+1qpn/taqZ/8C2pv/Z0cf/3dbM/93WzP/a0sj/wbep/7Wqmv+1qpn/urCf/9XOw//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9XOw/+8saH/taqZ/7Wqmf/Euqv/2tLI/9jRxv/Bt6j/taqZ/7Wqmf+5rp7/1s/F/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9fPxf+5rp7/taqZ/7Wqmf/Cuar/2dLH/9rSyP/Euqz/taqa/7Wqmf+8saH/1c7D/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/Ty7//vLGh/7Wqmf+2q5r/w7qr/8G3qP+1qpn/taqZ/7munv/Xz8T/7Obf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+zm3//Xz8T/ua6e/7Wqmf+1qpn/wrmq/8S6rP+2q5r/taqZ/7yxof/Ty7//3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/1s7D/7qwn/+1qpn/taqZ/7Wqmf+1qpn/tqua/9nSx//s5t//7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/s5t//2dLH/7armv+1qpn/taqZ/7Wqmv+1qpn/urCf/9bOw//d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9PLv/+3rZz/taqZ/7Wqmf+5rp7/2dLH/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9nSx/+5rp7/taqZ/7Wqmf+4rZz/1MvA/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/19DF/8C3qP+1qpn/taqZ/7munv/Xz8T/7Obf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+zm3//Xz8T/ua6e/7Wqmf+1qpn/wrip/9jRxv/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/X0MX/vrSl/7Wqmf+1qpn/tqua/9nSx//s5t//7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/s5t//2dLH/7armv+1qpn/taqZ/7+1pv/Y0cb/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9jRxv/At6j/taqZ/7Wqmf+5rp7/2NLH/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9nSx/+5rp7/taqZ/7Wqmf/Ct6n/2dHG/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/19DF/8C3qP+1qpn/taqZ/7munv/Wz8T/6+bf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+vm3//Wz8T/ua6e/7Wqmf+1qpn/wbep/9jRxv/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/X0MX/vrSl/7Wqmf+1qpn/tqua/9jRx//r5t//7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/r5t//2dLH/7armv+1qpn/taqZ/7+1pf/Y0cb/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9jRxv/At6j/taqZ/7Wqmf+5rp7/2NHH/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9nSx/+5rp7/taqZ/7Wqmf/Bt6j/2dHG/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/19DF/8C3qP+1qpn/taqZ/7munv/Wz8T/6+bf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+vm3//Wz8T/ua6e/7Wqmf+1qpn/wbeo/9jRxv/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/93WzP/X0MX/vrSl/7Wqmf+1qpn/tqua/9jRx//r5t//7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/r5t//2NLH/7armv+1qpn/taqZ/7+1pf/Y0cb/3dbM/93WzP/d1sz/3dbM/93WzP/d1sz/3dbM/9jRxv/At6j/taqZ/7Wqmf+5rp7/2NHH/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9jRx/+5rp7/taqZ/7Wqmf/Bt6j/2NHG/93WzP/d1sz/3dbM/93WzP/d1sz/19DF/8C3qP+1qpn/taqZ/7munv/Wz8T/6+bf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+vm3//Wz8T/ua6e/7Wqmf+1qpn/wbeo/9jQxf/d1sz/3dbM/93WzP/X0MX/vrSl/7Wqmf+1qpn/tqua/9jRxv/r5t//7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/r5t//2NLH/7armv+1qpn/taqZ/760pf/Y0MX/3dbM/9jRxv/At6j/taqZ/7Wqmf+5rp7/2NDG/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9jSx/+5r57/taqZ/7Wqmf/Bt6j/2NHG/8C3qP+1qpn/taqZ/7munv/Wz8P/6+bf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+vm3//Wz8T/ua+e/7Wqmf+1qpn/wbeo/7Wqmf+1qpn/tqua/9jRxv/r5t//7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/r5t//2NLH/7armv+1qpn/taqZ/7armvi5rp7/2NHG/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/9nSx/+5r57/tqua+LuxocDWzsP/6+bf/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+vm3//Wz8T/vbKiwNvRyFTr5t777ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/r5t7729XMVd3d3Q/s59+f7ejg+u3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4Prs59+f3d3dDwAAAADk5Nsc6ubeg+zm4Nft6OH77ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH/7ejh/+3o4f/t6OH77Obg1+rm3oPk5NscAAAAAAAAAAAAAAAA1NTUBubm3R7s49425+DZSufg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2kvn4NpL5+DaS+fg2Urs49425ubdHtTU1AYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
data/layout/layout.css CHANGED
@@ -18,14 +18,73 @@
18
18
  color: #007bff !important;
19
19
  }
20
20
 
21
- #inbox {
22
- overflow-y: auto;
23
- height: 100%;
24
- max-height: 100%;
25
- display: none;
21
+ .inbox-container {
26
22
  width: 35rem;
27
23
  max-width: 35rem;
24
+ height: calc(100vh - 6rem);
25
+ max-height: calc(100vh - 6rem);
26
+ overflow-y: auto;
27
+ display: none;
28
+ }
29
+
30
+ .inbox-header {
31
+ padding-left: 0.8rem;
32
+ padding-right: 0.8rem;
33
+ padding-top: 0.9rem;
34
+ padding-bottom: 0.2rem;
35
+ background-color: #fff;
36
+ border: 1px solid #ddd;
37
+ height: 3.5rem;
38
+ position: fixed;
39
+ z-index: 100;
40
+ width: 32rem;
41
+ max-width: 32rem;
42
+ }
43
+
44
+ #inbox-info {
45
+ font-size: 0.7rem;
46
+ margin-left: 0.2rem;
47
+ display: inline-block;
48
+ vertical-align: middle;
49
+ }
50
+
51
+ .inbox-loading {
52
+ font-size: 1.6rem;
53
+ font-weight: 200;
54
+ padding-top: 4rem;
55
+ padding-left: 0.2rem;
56
+ animation: fade 3s infinite;
57
+ text-align: center;
58
+ }
59
+
60
+ @keyframes fade {
61
+ 0% {
62
+ color: #555;
63
+ }
64
+ 50% {
65
+ color: #ddd;
66
+ }
67
+ 100% {
68
+ color: #555;
69
+ }
70
+ }
71
+
72
+ .show-hide-read-button {
73
+ position: absolute;
74
+ right: 5rem;
75
+ top: 0.4rem;
76
+ }
77
+
78
+ .read-all-button {
79
+ position: absolute;
80
+ right: 0.5rem;
81
+ top: 0.4rem;
82
+ }
83
+
84
+ #inbox {
28
85
  padding-right: 0.2rem;
86
+ padding-left: 0;
87
+ padding-top: 4rem;
29
88
  }
30
89
 
31
90
  #inbox.visible {
@@ -42,24 +101,59 @@
42
101
  top: 0.8rem;
43
102
  }
44
103
 
104
+ #inbox ul {
105
+ list-style-type: none;
106
+ }
107
+
108
+ .list-group-item {
109
+ padding-left: 0.75rem;
110
+ }
111
+
112
+ .inbox-container.hide-read li {
113
+ display: none;
114
+ }
115
+
116
+ .inbox-container.hide-read li.unread, .inbox-container.hide-read li.active {
117
+ display: block;
118
+ }
119
+
120
+ .inbox-container li .unread-icon, .inbox-container li .read-icon {
121
+ padding-right: 0.75rem;
122
+ font-size: 1.2rem;
123
+ }
124
+
125
+ #inbox li .read-icon {
126
+ opacity: 0.1;
127
+ display: inline;
128
+ }
129
+
130
+ #inbox li.active .read-icon {
131
+ color: #fff;
132
+ opacity: 0.3;
133
+ }
134
+
135
+ #inbox li .unread-icon {
136
+ display: none;
137
+ }
138
+
139
+ #inbox li.unread .read-icon {
140
+ display: none;
141
+ }
142
+
143
+ #inbox li.unread .unread-icon {
144
+ display: inline;
145
+ }
146
+
45
147
  #inbox li a {
46
148
  text-decoration: none;
47
149
  background-color: transparent;
48
- max-width: 22rem;
150
+ max-width: 23rem;
49
151
  overflow: hidden;
50
152
  display: block;
51
153
  white-space: nowrap;
52
154
  text-overflow: ellipsis
53
155
  }
54
156
 
55
- #inbox li a .unread-icon {
56
- color: #007bff;
57
- }
58
-
59
- #inbox li a:visited .unread-icon {
60
- color: #ffffff00;
61
- }
62
-
63
157
  #inbox li a:hover {
64
158
  text-decoration: none;
65
159
  }
data/layout/layout.js CHANGED
@@ -4,14 +4,25 @@
4
4
  let reloadIdentityIframeTimeout;
5
5
  let identityUuid;
6
6
  let indexUuid;
7
- let inboxInitialized = false;
8
-
9
- var htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
10
- var indexIframe = document.querySelector("#index-iframe");
11
- var identityIframe = document.querySelector("#identity-iframe");
12
- var textIframeDocument = document.querySelector("#text-iframe").contentDocument;
13
- var sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
14
- var sourceHighlightBundle = [
7
+ let twoColumnView;
8
+ let headersView;
9
+ let indexIframeTimeout;
10
+
11
+ const inboxContent = [];
12
+ const headers = document.querySelector('.headers');
13
+ const inbox = document.querySelector('#inbox-container');
14
+ const inboxInfo = document.querySelector('#inbox-info');
15
+ const columnSwitch = document.querySelector('.column-switch');
16
+ const headersViewSwitch = document.querySelector('.headers-view-switch');
17
+ const readAllButton = document.querySelector('.read-all-button');
18
+ const showHideReadButton = document.querySelector('.show-hide-read-button');
19
+ const showHideReadIcon = document.querySelector('.show-hide-read-icon');
20
+ const htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
21
+ const indexIframe = document.querySelector("#index-iframe");
22
+ const identityIframe = document.querySelector("#identity-iframe");
23
+ const textIframeDocument = document.querySelector("#text-iframe").contentDocument;
24
+ const sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
25
+ const sourceHighlightBundle = [
15
26
  '<link rel="stylesheet"',
16
27
  ' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
17
28
  ' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
@@ -22,52 +33,55 @@
22
33
  '<script>hljs.initHighlightingOnLoad();</' + 'script>',
23
34
  ].join('\n');
24
35
 
25
- const initialize = () => {
26
- loadMail(initialData);
36
+ const storage = window.localStorage;
27
37
 
38
+ const initialize = () => {
28
39
  reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
29
40
 
30
- window.addEventListener('message', function (ev) {
31
- switch (ev.data.type) {
32
- case 'index':
33
- renderInbox(ev.data.uuid, ev.data.mails);
34
- break;
35
- case 'identity':
36
- compareIdentity(ev.data.uuid);
37
- break;
38
- };
39
- });
40
-
41
- setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
42
41
  setInterval(function () { identityIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
43
42
 
44
- toolbar.html.onclick = function (ev) { setView('html', ev); };
45
- toolbar.text.onclick = function (ev) { setView('text', ev); };
46
- toolbar.source.onclick = function (ev) { setView('source', ev); };
47
- toolbar.headers.onclick = function () { setHeadersView(!headersView); };
48
- columnSwitch.onclick = function () { setColumnView(!twoColumnView); };
43
+ toolbar.html.onclick = (ev) => setView('html', ev);
44
+ toolbar.text.onclick = (ev) => setView('text', ev);
45
+ toolbar.source.onclick = (ev) => setView('source', ev);
46
+ toolbar.headers.onclick = () => setHeadersView(!headersView);
47
+ columnSwitch.onclick = () => setColumnView(!twoColumnView);
48
+ readAllButton.onclick = () => markAllAsRead();
49
+ showHideReadButton.onclick = () => toggleHideReadMessages();
49
50
 
50
- if (hasHtml) {
51
+ if (POSTMORTEM.hasHtml) {
51
52
  setView('html');
52
53
  } else {
53
54
  setView('text');
54
55
  }
55
56
 
56
- setColumnView(false);
57
+ setColumnView(!POSTMORTEM.downloadedPreview);
57
58
  setHeadersView(true);
59
+ setEnabled(columnSwitch);
60
+ setOn(columnSwitch);
61
+ setHidden(inbox, POSTMORTEM.downloadedPreview);
58
62
 
59
- $('[data-toggle="tooltip"]').tooltip();
60
- }
63
+ if (POSTMORTEM.downloadedPreview) {
64
+ setHidden(toolbar.download, true);
65
+ setHidden(columnSwitch, true);
66
+ } else {
67
+ window.addEventListener('message', function (ev) {
68
+ switch (ev.data.type) {
69
+ case 'index':
70
+ loadInbox(ev.data.uuid, ev.data.mails);
71
+ break;
72
+ case 'identity':
73
+ compareIdentity(ev.data.uuid);
74
+ break;
75
+ };
76
+ });
77
+ }
61
78
 
79
+ loadMail(POSTMORTEM.initialData);
62
80
 
63
- var twoColumnView;
64
- var headersView;
65
- var headers = document.querySelector('.headers');
66
- var inbox = document.querySelector('#inbox');
67
- var columnSwitch = document.querySelector('.column-switch');
68
- var headersViewSwitch = document.querySelector('.headers-view-switch');
81
+ $('[data-toggle="tooltip"]').tooltip();
82
+ }
69
83
 
70
- var setHeadersView = function (enableHeadersView) {
84
+ const setHeadersView = (enableHeadersView) => {
71
85
  headersView = enableHeadersView;
72
86
  if (enableHeadersView) {
73
87
  setOn(headersViewSwitch);
@@ -78,11 +92,11 @@
78
92
  }
79
93
  };
80
94
 
81
- var setColumnView = function (enableTwoColumnView) {
82
- if (!inboxInitialized) return;
95
+ const setColumnView = (enableTwoColumnView) => {
96
+ if (!inbox) return;
83
97
 
84
- var container = document.querySelector('.container');
85
- twoColumnView = enableTwoColumnView;
98
+ const container = document.querySelector('.container');
99
+ twoColumnView = POSTMORTEM.downloadedPreview ? false : enableTwoColumnView;
86
100
  if (twoColumnView) {
87
101
  setVisible(inbox, true);
88
102
  setOn(columnSwitch);
@@ -94,42 +108,51 @@
94
108
  }
95
109
  };
96
110
 
97
- var contexts = ['source', 'text', 'html'];
111
+ const contexts = ['source', 'text', 'html'];
98
112
 
99
- var views = {
113
+ const views = {
100
114
  source: document.querySelector('.source-view'),
101
115
  html: document.querySelector('.html-view'),
102
116
  text: document.querySelector('.text-view'),
103
117
  };
104
118
 
105
- var toolbar = {
119
+ const toolbar = {
106
120
  source: document.querySelector('.source-view-switch'),
107
121
  html: document.querySelector('.html-view-switch'),
108
122
  text: document.querySelector('.text-view-switch'),
109
123
  headers: document.querySelector('.headers-view-switch'),
124
+ download: document.querySelector('#download-link'),
110
125
  };
111
126
 
112
- var setOn = function(element) {
127
+ const setOn = function(element) {
113
128
  element.classList.add('text-primary');
114
129
  element.classList.remove('text-secondary');
115
130
  };
116
131
 
117
- var setOff = function(element) {
132
+ const setOff = function(element) {
118
133
  element.classList.add('text-secondary');
119
134
  element.classList.remove('text-primary');
120
135
  };
121
136
 
122
- var setDisabled = function(element) {
137
+ const setDisabled = function(element) {
123
138
  element.classList.add('disabled');
124
139
  element.classList.remove('text-secondary');
125
140
  };
126
141
 
127
- var setEnabled = function(element) {
142
+ const setEnabled = function(element) {
128
143
  element.classList.remove('disabled');
129
144
  element.classList.add('text-secondary');
130
145
  };
131
146
 
132
- var setVisible = function(element, visible) {
147
+ const setHidden = function(element, hidden) {
148
+ if (hidden) {
149
+ element.classList.add('hidden');
150
+ } else {
151
+ element.classList.remove('hidden');
152
+ }
153
+ };
154
+
155
+ const setVisible = function(element, visible) {
133
156
  if (visible) {
134
157
  element.classList.add('visible');
135
158
  } else {
@@ -137,9 +160,9 @@
137
160
  }
138
161
  };
139
162
 
140
- var setView = function(context, ev) {
163
+ const setView = function(context, ev) {
141
164
  if (ev && $(ev.target).hasClass('disabled')) return;
142
- var key;
165
+ let key;
143
166
  for (i = 0; i < contexts.length; i++) {
144
167
  key = contexts[i];
145
168
  if (key === context) {
@@ -194,11 +217,22 @@
194
217
  setEnabled(toolbar.source);
195
218
  }
196
219
 
197
- setDisabled(columnSwitch);
198
220
  setView(currentView);
199
221
  };
200
222
 
201
223
  const loadMail = (mail) => {
224
+ const initializeScript = document.querySelector("#initialize-script");
225
+ const initObject = {
226
+ initialData: mail,
227
+ hasHtml: !!mail.htmlBody,
228
+ hasText: !!mail.textBody,
229
+ downloadedPreview: true,
230
+ };
231
+
232
+ initializeScript.text = [
233
+ `const POSTMORTEM = ${JSON.stringify(initObject)};`
234
+ ].join('\n\n');
235
+
202
236
  htmlIframeDocument.open();
203
237
  htmlIframeDocument.write(mail.htmlBody);
204
238
  htmlIframeDocument.close();
@@ -215,10 +249,73 @@
215
249
  loadHeaders(mail);
216
250
  loadToolbar(mail);
217
251
  loadDownloadLink();
252
+
253
+ highlightMail(mail);
254
+ markAsRead(mail);
255
+ updateInboxInfo();
256
+ };
257
+
258
+ const markAsRead = (mail) => {
259
+ storage.setItem(mail.id, 'read');
260
+ $(`li[data-email-id="${mail.id}"]`).removeClass('unread');
261
+ $(readAllButton).blur();
262
+ };
263
+
264
+ const showReadMessages = (show) => {
265
+ if (show) {
266
+ inbox.classList.remove('hide-read');
267
+ } else {
268
+ inbox.classList.add('hide-read');
269
+ }
270
+ };
271
+
272
+ const toggleHideReadMessages = () => {
273
+ const $target = $(showHideReadButton);
274
+ const $icon = $(showHideReadIcon);
275
+
276
+ if ($target.data('state') === 'hide') {
277
+ $target.data('state', 'show');
278
+ $target.attr('data-original-title', 'Hide read messages');
279
+ $target.attr('title', 'Hide read messages');
280
+ $icon.removeClass('fa-eye-slash');
281
+ $icon.addClass('fa fa-eye text-primary');
282
+ showReadMessages(true);
283
+ } else {
284
+ $target.data('state', 'hide');
285
+ $target.attr('data-original-title', 'Show read messages');
286
+ $target.attr('title', 'Show read messages');
287
+ $icon.removeClass('fa-eye text-primary');
288
+ $icon.addClass('fa fa-eye-slash');
289
+ showReadMessages(false);
290
+ }
291
+
292
+ $target.blur();
293
+ };
294
+
295
+ const markAllAsRead = () => {
296
+ inboxContent.forEach(mail => markAsRead(mail));
297
+ updateInboxInfo();
298
+ };
299
+
300
+ const isNewMail = (mail) => {
301
+ if (!storage.getItem(mail.id)) return true;
302
+
303
+ return false;
304
+ };
305
+
306
+ const highlightMail = (mail) => {
307
+ window.location.hash = mail.id;
308
+ const $target = $(`li[data-email-id="${mail.id}"]`);
309
+ $('.inbox-item').removeClass('active');
310
+ $target.addClass('active');
218
311
  };
219
312
 
220
313
  const loadDownloadLink = () => {
221
- const blob = new Blob([document.documentElement.innerHTML], { type: 'application/octet-stream' });
314
+ const html = document.documentElement.innerHTML;
315
+ const start = html.indexOf('<!--INBOX-START-->');
316
+ const end = html.indexOf('<!--INBOX-END-->') + '<!--INBOX-END-->'.length;
317
+ const modifiedHtml = [html.substring(0, start), html.substring(end + 1, html.length)].join('');
318
+ const blob = new Blob([modifiedHtml], { type: 'application/octet-stream' });
222
319
  const uri = window.URL.createObjectURL(blob);
223
320
  $("#download-link").attr('href', uri);
224
321
  };
@@ -226,11 +323,26 @@
226
323
  const compareIdentity = (uuid) => {
227
324
  clearTimeout(reloadIdentityIframeTimeout);
228
325
  reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
229
- if (identityUuid !== uuid) indexIframe.src += '';
326
+ if (identityUuid !== uuid) {
327
+ indexIframe.src += '';
328
+ clearTimeout(indexIframeTimeout);
329
+ indexIframeTimeout = setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 200);
330
+ }
230
331
  identityUuid = uuid;
231
332
  };
232
333
 
233
- const renderInbox = (uuid, mails) => {
334
+ const updateInboxInfo = () => {
335
+ if (!inboxContent.length) return;
336
+
337
+ const unreadCount = inboxContent.filter((mail) => isNewMail(mail)).length;
338
+ document.title = `PostMortem ${unreadCount}/${inboxContent.length} (unread/total)`;
339
+ inboxInfo.textContent = `${inboxContent.length} emails (${unreadCount} unread)`;
340
+ inboxInfo.innerHTML = `&mdash; ${inboxInfo.innerHTML}`;
341
+ };
342
+
343
+ const loadInbox = (uuid, mails) => {
344
+ clearTimeout(indexIframeTimeout);
345
+ inboxContent.splice(0, Infinity, ...mails)
234
346
  if (uuid === indexUuid) {
235
347
  return;
236
348
  }
@@ -239,28 +351,29 @@
239
351
  const parsedTimestamp = new Date(mail.timestamp);
240
352
  const timestampSpan = `<span class="timestamp">${parsedTimestamp.toLocaleString()}</span>`;
241
353
  const classes = ['list-group-item', 'inbox-item'];
354
+
242
355
  if (window.location.hash === '#' + mail.id) classes.push('active');
356
+ if (isNewMail(mail)) classes.push('unread');
357
+
243
358
  mailsById[mail.id] = mail;
244
- return `<li data-email-id="${mail.id}" class="${classes.join(' ')}"><a title="${mail.subject}" href="javascript:void(0)">${mail.subject}</a>${timestampSpan}</li>`
359
+
360
+ return [`<li data-email-id="${mail.id}" class="${classes.join(' ')}">`,
361
+ `<a title="${mail.subject}" href="javascript:void(0)">`,
362
+ `<i class="fa fa-envelope-open read-icon"></i>`,
363
+ `<i class="fa fa-envelope unread-icon"></i>${mail.subject}`,
364
+ `</a>`,
365
+ `${timestampSpan}</li>`].join('');
245
366
  });
367
+ updateInboxInfo();
246
368
  if (arrayIdentical(html, previousInbox)) return;
247
369
  previousInbox = html;
248
370
  $('#inbox').html('<ul class="list-group">' + html.join('\n') + '</ul>');
249
371
  $('.inbox-item').click((ev) => {
250
372
  const $target = $(ev.currentTarget);
251
373
  const id = $target.data('email-id');
252
- $('.inbox-item').removeClass('active');
253
- $target.addClass('active');
254
- window.location.hash = id;
255
374
  setTimeout(() => loadMail(mailsById[id].content), 0);
256
375
  });
257
376
 
258
- if (!inboxInitialized) {
259
- setEnabled(columnSwitch);
260
- setColumnView(true);
261
- setVisible(inbox, true);
262
- }
263
- inboxInitialized = true;
264
377
  indexUuid = uuid;
265
378
  };
266
379
 
data/lib/postmortem.rb CHANGED
@@ -9,6 +9,7 @@ require 'erb'
9
9
  require 'json'
10
10
  require 'cgi'
11
11
  require 'digest'
12
+ require 'securerandom'
12
13
 
13
14
  require 'postmortem/version'
14
15
  require 'postmortem/adapters'
@@ -56,7 +57,7 @@ module Postmortem
56
57
  private
57
58
 
58
59
  def log_delivery(delivery)
59
- output_file.write(colorized(delivery.path.to_s) + "\n")
60
+ output_file.write("#{colorized(delivery.path.to_s)}\n")
60
61
  output_file.flush
61
62
  end
62
63
 
@@ -67,7 +68,7 @@ module Postmortem
67
68
  end
68
69
 
69
70
  def output_file
70
- return STDOUT if config.log_path.nil?
71
+ return $stdout if config.log_path.nil?
71
72
 
72
73
  @output_file ||= File.open(config.log_path, mode: 'a')
73
74
  end
@@ -6,7 +6,7 @@ require 'postmortem/adapters/mail'
6
6
  require 'postmortem/adapters/pony'
7
7
 
8
8
  module Postmortem
9
- # Adapters for various email senders (e.g. ActionMailer).
9
+ # Adapters for various email senders (e.g. Mail, Pony).
10
10
  module Adapters
11
11
  end
12
12
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Postmortem
4
4
  module Adapters
5
- FIELDS = %i[from reply_to to cc bcc subject text_body html_body].freeze
5
+ FIELDS = %i[from reply_to to cc bcc subject text_body html_body message_id].freeze
6
6
 
7
7
  # Base interface implementation for all Postmortem adapters.
8
8
  class Base
@@ -16,7 +16,11 @@ module Postmortem
16
16
  end
17
17
 
18
18
  def serializable
19
- FIELDS.map { |field| [camelize(field.to_s), public_send(field)] }.to_h
19
+ (%i[id] + FIELDS).map { |field| [camelize(field.to_s), public_send(field)] }.to_h
20
+ end
21
+
22
+ def id
23
+ @id ||= SecureRandom.uuid
20
24
  end
21
25
 
22
26
  FIELDS.each do |method_name|
@@ -7,10 +7,10 @@ module Postmortem
7
7
  private
8
8
 
9
9
  def adapted
10
- %i[from reply_to to cc bcc subject]
11
- .map { |field| [field, @data.public_send(field)] }
10
+ %i[from reply_to to cc bcc subject message_id]
11
+ .map { |field| [field, mail.public_send(field)] }
12
12
  .to_h
13
- .merge({ text_body: @data.text_part&.decoded, html_body: @data.html_part&.decoded })
13
+ .merge({ text_body: mail.text_part&.decoded, html_body: mail.html_part&.decoded })
14
14
  end
15
15
 
16
16
  def mail
@@ -8,14 +8,11 @@ module Postmortem
8
8
 
9
9
  def adapted
10
10
  {
11
- from: mail.from,
12
- reply_to: mail.reply_to,
13
- to: mail.to,
14
- cc: mail.cc,
15
- bcc: mail.bcc,
11
+ from: mail.from, reply_to: mail.reply_to, to: mail.to, cc: mail.cc, bcc: mail.bcc,
16
12
  subject: mail.subject,
17
13
  text_body: @data[:body],
18
- html_body: @data[:html_body]
14
+ html_body: @data[:html_body],
15
+ message_id: mail.message_id # We use a synthetic Mail instance so this is a bit useless.
19
16
  }
20
17
  end
21
18
 
@@ -35,7 +35,7 @@ module Postmortem
35
35
  end
36
36
 
37
37
  def encoded_mail
38
- Base64.encode64(mail_data.merge(id: Digest::MD5.hexdigest(mail_data.to_json)).to_json).split("\n").join
38
+ Base64.encode64(mail_data.to_json).split("\n").join
39
39
  end
40
40
 
41
41
  def mail_data
@@ -43,6 +43,7 @@ module Postmortem
43
43
  subject: @mail.subject || '(no subject)',
44
44
  timestamp: timestamp,
45
45
  path: @mail_path,
46
+ id: @mail.id,
46
47
  content: @mail.serializable
47
48
  }
48
49
  end
@@ -37,6 +37,10 @@ module Postmortem
37
37
  default_layout_directory.join('headers_template.html').read
38
38
  end
39
39
 
40
+ def favicon_b64
41
+ default_layout_directory.join('favicon.b64').read
42
+ end
43
+
40
44
  private
41
45
 
42
46
  def default_layout_directory
@@ -1,5 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  ActiveSupport::Notifications.subscribe 'deliver.action_mailer' do |*args|
4
+ delivery_method = Rails.try(:application)
5
+ &.try(:config)
6
+ &.try(:action_mailer)
7
+ &.try(:delivery_method)
8
+ next if delivery_method.nil?
9
+ next if %i[sendmail smtp].include?(delivery_method&.to_sym) # Delegate to Mail plugin.
10
+
4
11
  Postmortem.record_delivery(Postmortem::Adapters::ActionMailer.new(args.extract_options!))
5
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Postmortem
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.4'
5
5
  end
data/postmortem.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'Preview HTML emails in your browser during development'
13
13
  spec.homepage = 'https://github.com/bobf/postmortem'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
16
16
 
17
17
  spec.metadata['homepage_uri'] = spec.homepage
18
18
  spec.metadata['source_code_uri'] = spec.homepage
@@ -28,10 +28,12 @@ Gem::Specification.new do |spec|
28
28
  spec.add_runtime_dependency 'mail', '~> 2.7'
29
29
 
30
30
  spec.add_development_dependency 'actionmailer', '~> 6.0'
31
+ spec.add_development_dependency 'devpack', '~> 0.3.2'
31
32
  spec.add_development_dependency 'pony', '~> 1.13'
32
33
  spec.add_development_dependency 'rspec', '~> 3.9'
33
34
  spec.add_development_dependency 'rspec-its', '~> 1.3'
34
- spec.add_development_dependency 'rubocop', '~> 0.88.0'
35
+ spec.add_development_dependency 'rubocop', '~> 1.10'
36
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.2'
35
37
  spec.add_development_dependency 'strong_versions', '~> 0.4.5'
36
38
  spec.add_development_dependency 'timecop', '~> 0.9.1'
37
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postmortem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-24 00:00:00.000000000 Z
11
+ date: 2021-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mail
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: devpack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.3.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.2
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: pony
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +100,28 @@ dependencies:
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: 0.88.0
103
+ version: '1.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.10'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.2'
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
- version: 0.88.0
124
+ version: '2.2'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: strong_versions
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -144,6 +172,7 @@ files:
144
172
  - layout/default.html.erb
145
173
  - layout/dependencies.css
146
174
  - layout/dependencies.js
175
+ - layout/favicon.b64
147
176
  - layout/headers_template.html
148
177
  - layout/layout.css
149
178
  - layout/layout.js
@@ -180,7 +209,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
209
  requirements:
181
210
  - - ">="
182
211
  - !ruby/object:Gem::Version
183
- version: 2.3.0
212
+ version: 2.5.0
184
213
  required_rubygems_version: !ruby/object:Gem::Requirement
185
214
  requirements:
186
215
  - - ">="
@@ -188,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
217
  version: '0'
189
218
  requirements: []
190
219
  rubyforge_project:
191
- rubygems_version: 2.7.6
220
+ rubygems_version: 2.7.6.2
192
221
  signing_key:
193
222
  specification_version: 4
194
223
  summary: Development HTML Email Inspection Tool