mailcatcher-ng 1.4.6 → 1.5.2
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 +4 -4
- data/README.md +66 -0
- data/lib/mail_catcher/integrations/mcp_server.rb +187 -0
- data/lib/mail_catcher/integrations/mcp_tools.rb +370 -0
- data/lib/mail_catcher/integrations.rb +63 -0
- data/lib/mail_catcher/mail.rb +328 -0
- data/lib/mail_catcher/version.rb +1 -1
- data/lib/mail_catcher/web/application.rb +442 -0
- data/lib/mail_catcher.rb +42 -1
- data/public/assets/mailcatcher.css +154 -0
- data/public/assets/mailcatcher.js +176 -0
- data/views/index.erb +29 -0
- metadata +18 -1
|
@@ -821,11 +821,187 @@ class MailCatcher {
|
|
|
821
821
|
|
|
822
822
|
$("#message .views .download a").attr("href", `messages/${id}.eml`);
|
|
823
823
|
|
|
824
|
+
this.loadAccessibilityScore(id);
|
|
824
825
|
this.loadMessageBody();
|
|
825
826
|
});
|
|
826
827
|
}
|
|
827
828
|
}
|
|
828
829
|
|
|
830
|
+
loadAccessibilityScore(id) {
|
|
831
|
+
$.getJSON(`messages/${id}/accessibility.json`, (data) => {
|
|
832
|
+
// Update accessibility score
|
|
833
|
+
$("#accessibilityScore").text(data.score);
|
|
834
|
+
|
|
835
|
+
// Update breakdown metrics
|
|
836
|
+
if (data.breakdown) {
|
|
837
|
+
$("#imagesWithAlt").text(data.breakdown.images_with_alt + "%");
|
|
838
|
+
$("#semanticHtml").text(data.breakdown.semantic_html + "%");
|
|
839
|
+
$("#linksWithText").text(data.breakdown.links_with_text + "%");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Initialize Tippy tooltips with detailed findings
|
|
843
|
+
this.initAccessibilityTooltips(data);
|
|
844
|
+
}).fail(() => {
|
|
845
|
+
// Reset scores if API call fails (e.g., for plain text emails)
|
|
846
|
+
$("#accessibilityScore").text("-");
|
|
847
|
+
$("#imagesWithAlt").text("-");
|
|
848
|
+
$("#semanticHtml").text("-");
|
|
849
|
+
$("#linksWithText").text("-");
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
initAccessibilityTooltips(data) {
|
|
854
|
+
// Main accessibility score tooltip
|
|
855
|
+
tippy("#accessibilityScoreBtn", {
|
|
856
|
+
content: this.buildAccessibilityTooltip(data),
|
|
857
|
+
allowHTML: true,
|
|
858
|
+
interactive: true,
|
|
859
|
+
theme: 'light',
|
|
860
|
+
placement: 'bottom',
|
|
861
|
+
maxWidth: 400,
|
|
862
|
+
sticky: true
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Images with alt text tooltip
|
|
866
|
+
if (data.findings && data.findings.images) {
|
|
867
|
+
tippy("#imagesWithAltBtn", {
|
|
868
|
+
content: this.buildImagesTooltiip(data.findings.images),
|
|
869
|
+
allowHTML: true,
|
|
870
|
+
interactive: true,
|
|
871
|
+
theme: 'light',
|
|
872
|
+
placement: 'bottom',
|
|
873
|
+
maxWidth: 400,
|
|
874
|
+
sticky: true
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Semantic HTML tooltip
|
|
879
|
+
if (data.findings && data.findings.semantic) {
|
|
880
|
+
tippy("#semanticHtmlBtn", {
|
|
881
|
+
content: this.buildSemanticTooltip(data.findings.semantic),
|
|
882
|
+
allowHTML: true,
|
|
883
|
+
interactive: true,
|
|
884
|
+
theme: 'light',
|
|
885
|
+
placement: 'bottom',
|
|
886
|
+
maxWidth: 400,
|
|
887
|
+
sticky: true
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Links with text tooltip
|
|
892
|
+
if (data.findings && data.findings.links) {
|
|
893
|
+
tippy("#linksWithTextBtn", {
|
|
894
|
+
content: this.buildLinksTooltip(data.findings.links),
|
|
895
|
+
allowHTML: true,
|
|
896
|
+
interactive: true,
|
|
897
|
+
theme: 'light',
|
|
898
|
+
placement: 'bottom',
|
|
899
|
+
maxWidth: 400,
|
|
900
|
+
sticky: true
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
buildAccessibilityTooltip(data) {
|
|
906
|
+
let html = `<div class="accessibility-tooltip">
|
|
907
|
+
<h4>Overall Accessibility Score: ${data.score}/100</h4>`;
|
|
908
|
+
|
|
909
|
+
if (data.recommendations && data.recommendations.length) {
|
|
910
|
+
html += `<div class="tooltip-section">
|
|
911
|
+
<h5>Recommendations:</h5>
|
|
912
|
+
<ul>`;
|
|
913
|
+
data.recommendations.forEach(rec => {
|
|
914
|
+
html += `<li>${rec}</li>`;
|
|
915
|
+
});
|
|
916
|
+
html += `</ul></div>`;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
html += `</div>`;
|
|
920
|
+
return html;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
buildImagesTooltiip(findings) {
|
|
924
|
+
let html = `<div class="accessibility-tooltip">
|
|
925
|
+
<h4>Image Accessibility</h4>
|
|
926
|
+
<div class="tooltip-stats">
|
|
927
|
+
<div><strong>Total images:</strong> ${findings.total}</div>
|
|
928
|
+
<div><strong>With alt text:</strong> ${findings.with_alt}</div>
|
|
929
|
+
<div><strong>Missing alt text:</strong> ${findings.total - findings.with_alt}</div>
|
|
930
|
+
</div>`;
|
|
931
|
+
|
|
932
|
+
if (findings.without_alt && findings.without_alt.length > 0) {
|
|
933
|
+
html += `<div class="tooltip-section">
|
|
934
|
+
<h5>Images without alt text:</h5>
|
|
935
|
+
<ul class="tooltip-issues">`;
|
|
936
|
+
findings.without_alt.slice(0, 5).forEach(img => {
|
|
937
|
+
html += `<li>${img.alt_missing ? '❌ Missing alt attribute' : '⚠️ Empty alt text'}: ${img.src || '(no src)'}</li>`;
|
|
938
|
+
});
|
|
939
|
+
if (findings.without_alt.length > 5) {
|
|
940
|
+
html += `<li>... and ${findings.without_alt.length - 5} more</li>`;
|
|
941
|
+
}
|
|
942
|
+
html += `</ul></div>`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
html += `</div>`;
|
|
946
|
+
return html;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
buildSemanticTooltip(findings) {
|
|
950
|
+
let html = `<div class="accessibility-tooltip">
|
|
951
|
+
<h4>Semantic HTML</h4>`;
|
|
952
|
+
|
|
953
|
+
if (findings.has_semantic_tags) {
|
|
954
|
+
html += `<div class="tooltip-stats">
|
|
955
|
+
<div>✅ Semantic tags found</div>`;
|
|
956
|
+
if (findings.found_tags && findings.found_tags.length) {
|
|
957
|
+
html += `<div><strong>Tags:</strong> ${findings.found_tags.join(', ')}</div>`;
|
|
958
|
+
}
|
|
959
|
+
html += `</div>`;
|
|
960
|
+
} else {
|
|
961
|
+
html += `<div class="tooltip-stats">
|
|
962
|
+
<div>⚠️ No semantic HTML tags found</div>
|
|
963
|
+
<div style="margin-top: 8px; font-size: 12px;">Consider using semantic tags like:</div>
|
|
964
|
+
<ul class="tooltip-tips">
|
|
965
|
+
<li><header> - Page header</li>
|
|
966
|
+
<li><nav> - Navigation area</li>
|
|
967
|
+
<li><main> - Main content</li>
|
|
968
|
+
<li><article> - Article content</li>
|
|
969
|
+
<li><section> - Content section</li>
|
|
970
|
+
<li><footer> - Page footer</li>
|
|
971
|
+
</ul>
|
|
972
|
+
</div>`;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
html += `</div>`;
|
|
976
|
+
return html;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
buildLinksTooltip(findings) {
|
|
980
|
+
let html = `<div class="accessibility-tooltip">
|
|
981
|
+
<h4>Link Accessibility</h4>
|
|
982
|
+
<div class="tooltip-stats">
|
|
983
|
+
<div><strong>Total links:</strong> ${findings.total}</div>
|
|
984
|
+
<div><strong>With descriptive text:</strong> ${findings.with_text}</div>
|
|
985
|
+
<div><strong>Missing text/aria-label:</strong> ${findings.total - findings.with_text}</div>
|
|
986
|
+
</div>`;
|
|
987
|
+
|
|
988
|
+
if (findings.without_text && findings.without_text.length > 0) {
|
|
989
|
+
html += `<div class="tooltip-section">
|
|
990
|
+
<h5>Links without descriptive text:</h5>
|
|
991
|
+
<ul class="tooltip-issues">`;
|
|
992
|
+
findings.without_text.slice(0, 5).forEach(link => {
|
|
993
|
+
html += `<li>${link.text_empty ? '❌ No text' : '⚠️ No aria-label'}: ${link.href || '(no href)'}</li>`;
|
|
994
|
+
});
|
|
995
|
+
if (findings.without_text.length > 5) {
|
|
996
|
+
html += `<li>... and ${findings.without_text.length - 5} more</li>`;
|
|
997
|
+
}
|
|
998
|
+
html += `</ul></div>`;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
html += `</div>`;
|
|
1002
|
+
return html;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
829
1005
|
loadMessageBody(id, format) {
|
|
830
1006
|
id = id || this.selectedMessage();
|
|
831
1007
|
format = format || $("#message .views .tab.format.selected").attr("data-message-format");
|
data/views/index.erb
CHANGED
|
@@ -191,6 +191,35 @@
|
|
|
191
191
|
</a></li>
|
|
192
192
|
</ul>
|
|
193
193
|
</nav>
|
|
194
|
+
<div class="accessibility-ratings">
|
|
195
|
+
<div class="accessibility-score">
|
|
196
|
+
<div class="score-label">Accessibility</div>
|
|
197
|
+
<button class="accessibility-score-btn" id="accessibilityScoreBtn" title="Click for accessibility details">
|
|
198
|
+
<div class="score-value" id="accessibilityScore">-</div>
|
|
199
|
+
</button>
|
|
200
|
+
<div class="score-unit">/ 100</div>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="accessibility-breakdown">
|
|
203
|
+
<div class="breakdown-item">
|
|
204
|
+
<span class="breakdown-label">Images with Alt Text</span>
|
|
205
|
+
<button class="accessibility-metric-btn" id="imagesWithAltBtn" title="Click for image accessibility details">
|
|
206
|
+
<span class="breakdown-value" id="imagesWithAlt">-</span>
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="breakdown-item">
|
|
210
|
+
<span class="breakdown-label">Semantic HTML</span>
|
|
211
|
+
<button class="accessibility-metric-btn" id="semanticHtmlBtn" title="Click for semantic HTML details">
|
|
212
|
+
<span class="breakdown-value" id="semanticHtml">-</span>
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="breakdown-item">
|
|
216
|
+
<span class="breakdown-label">Link Text</span>
|
|
217
|
+
<button class="accessibility-metric-btn" id="linksWithTextBtn" title="Click for link accessibility details">
|
|
218
|
+
<span class="breakdown-value" id="linksWithText">-</span>
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
194
223
|
</div>
|
|
195
224
|
<div class="metadata">
|
|
196
225
|
<div class="metadata-column">
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailcatcher-ng
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephane Paquet
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: 0.5.1
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: nokogiri
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.18'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.18'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
83
|
name: ostruct
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -325,6 +339,9 @@ files:
|
|
|
325
339
|
- bin/mailcatcher
|
|
326
340
|
- lib/mail_catcher.rb
|
|
327
341
|
- lib/mail_catcher/bus.rb
|
|
342
|
+
- lib/mail_catcher/integrations.rb
|
|
343
|
+
- lib/mail_catcher/integrations/mcp_server.rb
|
|
344
|
+
- lib/mail_catcher/integrations/mcp_tools.rb
|
|
328
345
|
- lib/mail_catcher/mail.rb
|
|
329
346
|
- lib/mail_catcher/smtp.rb
|
|
330
347
|
- lib/mail_catcher/version.rb
|