email_signature_parser 0.1.2 → 0.1.4
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2525a79c98466992d96ab35de5d48b01ee2b582087db6e0a3f1b2fa7eb3825ae
|
|
4
|
+
data.tar.gz: 5b3b825f71b431d535d1d56ab2c3a1b51e74ddcec16ac964b7e98a85a24558f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e30da1515107da2c7975b480fbdaa867f989b874149147a3fdee2c14ffef787d9f89f2e2e4a7406588c7be126eacc603d80e18a1e92bdcc55e27527527a132da
|
|
7
|
+
data.tar.gz: c666ea83e31f75aedee3baffe4132c6aee1e6edd2f0827efa4ed9e0e998af03cff4482202382fa4d0e60c1ae9c82b73136a715c2a22bb122897773c4ed61f331
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# EmailSignatureParser
|
|
2
2
|
|
|
3
|
-
A Ruby gem for parsing email signatures. The gem tries to find the signature based on the name, if available, or email address and try to extract as much information as it can
|
|
3
|
+
A Ruby gem for parsing email signatures. The gem tries to find the signature based on the name, if available, or email address and try to extract as much information as it can
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -92,8 +92,8 @@ It will return a hash with whatever could be extracted from the signature
|
|
|
92
92
|
]
|
|
93
93
|
},
|
|
94
94
|
"job_title": {
|
|
95
|
-
"
|
|
96
|
-
"
|
|
95
|
+
"titles": ["Sales Marketing VP"],
|
|
96
|
+
"acronyms": ["CEO"]
|
|
97
97
|
},
|
|
98
98
|
"text": "Text of the signature",
|
|
99
99
|
"company_name": "TestCompany Ltd"
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
module EmailSignatureParser
|
|
2
|
+
class HtmlToMarkdownParser < ::Ox::Sax
|
|
3
|
+
attr_reader :parsed_text
|
|
4
|
+
HTML_SPECIAL_ESCAPED_CHARACTERS = {
|
|
5
|
+
'<' => '<',
|
|
6
|
+
'>' => '>',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
HTML_SPECIAL_CHARACTERS = {
|
|
10
|
+
# Reserved
|
|
11
|
+
'&' => '&',
|
|
12
|
+
'"' => '"',
|
|
13
|
+
''' => "'",
|
|
14
|
+
|
|
15
|
+
# ISO 8859-1 Symbols
|
|
16
|
+
' ' => ' ',
|
|
17
|
+
'¡' => '¡',
|
|
18
|
+
'¢' => '¢',
|
|
19
|
+
'£' => '£',
|
|
20
|
+
'¤' => '¤',
|
|
21
|
+
'¥' => '¥',
|
|
22
|
+
'¦' => '¦',
|
|
23
|
+
'§' => '§',
|
|
24
|
+
'¨' => '¨',
|
|
25
|
+
'©' => '©',
|
|
26
|
+
'ª' => 'ª',
|
|
27
|
+
'«' => '«',
|
|
28
|
+
'¬' => '¬',
|
|
29
|
+
'­' => '',
|
|
30
|
+
'®' => '®',
|
|
31
|
+
'¯' => '¯',
|
|
32
|
+
'°' => '°',
|
|
33
|
+
'±' => '±',
|
|
34
|
+
'²' => '²',
|
|
35
|
+
'³' => '³',
|
|
36
|
+
'´' => '´',
|
|
37
|
+
'µ' => 'µ',
|
|
38
|
+
'¶' => '¶',
|
|
39
|
+
'·' => '·',
|
|
40
|
+
'¸' => '¸',
|
|
41
|
+
'¹' => '¹',
|
|
42
|
+
'º' => 'º',
|
|
43
|
+
'»' => '»',
|
|
44
|
+
'¼' => '¼',
|
|
45
|
+
'½' => '½',
|
|
46
|
+
'¾' => '¾',
|
|
47
|
+
'¿' => '¿',
|
|
48
|
+
'×' => '×',
|
|
49
|
+
'÷' => '÷',
|
|
50
|
+
|
|
51
|
+
# ISO 8859-1 Characters
|
|
52
|
+
'À' => 'À',
|
|
53
|
+
'Á' => 'Á',
|
|
54
|
+
'Â' => 'Â',
|
|
55
|
+
'Ã' => 'Ã',
|
|
56
|
+
'Ä' => 'Ä',
|
|
57
|
+
'Å' => 'Å',
|
|
58
|
+
'Æ' => 'Æ',
|
|
59
|
+
'Ç' => 'Ç',
|
|
60
|
+
'È' => 'È',
|
|
61
|
+
'É' => 'É',
|
|
62
|
+
'Ê' => 'Ê',
|
|
63
|
+
'Ë' => 'Ë',
|
|
64
|
+
'Ì' => 'Ì',
|
|
65
|
+
'Í' => 'Í',
|
|
66
|
+
'Î' => 'Î',
|
|
67
|
+
'Ï' => 'Ï',
|
|
68
|
+
'Ð' => 'Ð',
|
|
69
|
+
'Ñ' => 'Ñ',
|
|
70
|
+
'Ò' => 'Ò',
|
|
71
|
+
'Ó' => 'Ó',
|
|
72
|
+
'Ô' => 'Ô',
|
|
73
|
+
'Õ' => 'Õ',
|
|
74
|
+
'Ö' => 'Ö',
|
|
75
|
+
'Ø' => 'Ø',
|
|
76
|
+
'Ù' => 'Ù',
|
|
77
|
+
'Ú' => 'Ú',
|
|
78
|
+
'Û' => 'Û',
|
|
79
|
+
'Ü' => 'Ü',
|
|
80
|
+
'Ý' => 'Ý',
|
|
81
|
+
'Þ' => 'Þ',
|
|
82
|
+
'ß' => 'ß',
|
|
83
|
+
'à' => 'à',
|
|
84
|
+
'á' => 'á',
|
|
85
|
+
'â' => 'â',
|
|
86
|
+
'ã' => 'ã',
|
|
87
|
+
'ä' => 'ä',
|
|
88
|
+
'å' => 'å',
|
|
89
|
+
'æ' => 'æ',
|
|
90
|
+
'ç' => 'ç',
|
|
91
|
+
'è' => 'è',
|
|
92
|
+
'é' => 'é',
|
|
93
|
+
'ê' => 'ê',
|
|
94
|
+
'ë' => 'ë',
|
|
95
|
+
'ì' => 'ì',
|
|
96
|
+
'í' => 'í',
|
|
97
|
+
'î' => 'î',
|
|
98
|
+
'ï' => 'ï',
|
|
99
|
+
'ð' => 'ð',
|
|
100
|
+
'ñ' => 'ñ',
|
|
101
|
+
'ò' => 'ò',
|
|
102
|
+
'ó' => 'ó',
|
|
103
|
+
'ô' => 'ô',
|
|
104
|
+
'õ' => 'õ',
|
|
105
|
+
'ö' => 'ö',
|
|
106
|
+
'ø' => 'ø',
|
|
107
|
+
'ù' => 'ù',
|
|
108
|
+
'ú' => 'ú',
|
|
109
|
+
'û' => 'û',
|
|
110
|
+
'ü' => 'ü',
|
|
111
|
+
'ý' => 'ý',
|
|
112
|
+
'þ' => 'þ',
|
|
113
|
+
'ÿ' => 'ÿ',
|
|
114
|
+
|
|
115
|
+
# General HTML Symbols
|
|
116
|
+
'Œ' => 'Œ',
|
|
117
|
+
'œ' => 'œ',
|
|
118
|
+
'Š' => 'Š',
|
|
119
|
+
'š' => 'š',
|
|
120
|
+
'Ÿ' => 'Ÿ',
|
|
121
|
+
'ƒ' => 'ƒ',
|
|
122
|
+
'ˆ' => 'ˆ',
|
|
123
|
+
'˜' => '˜',
|
|
124
|
+
' ' => ' ',
|
|
125
|
+
' ' => ' ',
|
|
126
|
+
' ' => ' ',
|
|
127
|
+
'‌' => '',
|
|
128
|
+
'‍' => '',
|
|
129
|
+
'‎' => '',
|
|
130
|
+
'‏' => '',
|
|
131
|
+
'–' => '–',
|
|
132
|
+
'—' => '—',
|
|
133
|
+
'‘' => "\u2018",
|
|
134
|
+
'’' => "\u2019",
|
|
135
|
+
'‚' => '‚',
|
|
136
|
+
'“' => '"',
|
|
137
|
+
'”' => '"',
|
|
138
|
+
'„' => '„',
|
|
139
|
+
'†' => '†',
|
|
140
|
+
'‡' => '‡',
|
|
141
|
+
'•' => '•',
|
|
142
|
+
'…' => '…',
|
|
143
|
+
'‰' => '‰',
|
|
144
|
+
'′' => '′',
|
|
145
|
+
'″' => '″',
|
|
146
|
+
'‹' => '‹',
|
|
147
|
+
'›' => '›',
|
|
148
|
+
'‾' => '‾',
|
|
149
|
+
'€' => '€',
|
|
150
|
+
'™' => '™',
|
|
151
|
+
'←' => '←',
|
|
152
|
+
'↑' => '↑',
|
|
153
|
+
'→' => '→',
|
|
154
|
+
'↓' => '↓',
|
|
155
|
+
'↔' => '↔',
|
|
156
|
+
'↵' => '↵',
|
|
157
|
+
'⌈' => '⌈',
|
|
158
|
+
'⌉' => '⌉',
|
|
159
|
+
'⌊' => '⌊',
|
|
160
|
+
'⌋' => '⌋',
|
|
161
|
+
'◊' => '◊',
|
|
162
|
+
'♠' => '♠',
|
|
163
|
+
'♣' => '♣',
|
|
164
|
+
'♥' => '♥',
|
|
165
|
+
'♦' => '♦',
|
|
166
|
+
|
|
167
|
+
# Mathematical Symbols
|
|
168
|
+
'∀' => '∀',
|
|
169
|
+
'∂' => '∂',
|
|
170
|
+
'∃' => '∃',
|
|
171
|
+
'∅' => '∅',
|
|
172
|
+
'∇' => '∇',
|
|
173
|
+
'∈' => '∈',
|
|
174
|
+
'∉' => '∉',
|
|
175
|
+
'∋' => '∋',
|
|
176
|
+
'∏' => '∏',
|
|
177
|
+
'∑' => '∑',
|
|
178
|
+
'−' => '−',
|
|
179
|
+
'∗' => '∗',
|
|
180
|
+
'√' => '√',
|
|
181
|
+
'∝' => '∝',
|
|
182
|
+
'∞' => '∞',
|
|
183
|
+
'∠' => '∠',
|
|
184
|
+
'∧' => '∧',
|
|
185
|
+
'∨' => '∨',
|
|
186
|
+
'∩' => '∩',
|
|
187
|
+
'∪' => '∪',
|
|
188
|
+
'∫' => '∫',
|
|
189
|
+
'∴' => '∴',
|
|
190
|
+
'∼' => '∼',
|
|
191
|
+
'≅' => '≅',
|
|
192
|
+
'≈' => '≈',
|
|
193
|
+
'≠' => '≠',
|
|
194
|
+
'≡' => '≡',
|
|
195
|
+
'≤' => '≤',
|
|
196
|
+
'≥' => '≥',
|
|
197
|
+
'⊂' => '⊂',
|
|
198
|
+
'⊃' => '⊃',
|
|
199
|
+
'⊄' => '⊄',
|
|
200
|
+
'⊆' => '⊆',
|
|
201
|
+
'⊇' => '⊇',
|
|
202
|
+
'⊕' => '⊕',
|
|
203
|
+
'⊗' => '⊗',
|
|
204
|
+
'⊥' => '⊥',
|
|
205
|
+
'⋅' => '⋅',
|
|
206
|
+
|
|
207
|
+
# Greek Characters
|
|
208
|
+
'Α' => 'Α',
|
|
209
|
+
'Β' => 'Β',
|
|
210
|
+
'Γ' => 'Γ',
|
|
211
|
+
'Δ' => 'Δ',
|
|
212
|
+
'Ε' => 'Ε',
|
|
213
|
+
'Ζ' => 'Ζ',
|
|
214
|
+
'Η' => 'Η',
|
|
215
|
+
'Θ' => 'Θ',
|
|
216
|
+
'Ι' => 'Ι',
|
|
217
|
+
'Κ' => 'Κ',
|
|
218
|
+
'Λ' => 'Λ',
|
|
219
|
+
'Μ' => 'Μ',
|
|
220
|
+
'Ν' => 'Ν',
|
|
221
|
+
'Ξ' => 'Ξ',
|
|
222
|
+
'Ο' => 'Ο',
|
|
223
|
+
'Π' => 'Π',
|
|
224
|
+
'Ρ' => 'Ρ',
|
|
225
|
+
'Σ' => 'Σ',
|
|
226
|
+
'Τ' => 'Τ',
|
|
227
|
+
'Υ' => 'Υ',
|
|
228
|
+
'Φ' => 'Φ',
|
|
229
|
+
'Χ' => 'Χ',
|
|
230
|
+
'Ψ' => 'Ψ',
|
|
231
|
+
'Ω' => 'Ω',
|
|
232
|
+
'α' => 'α',
|
|
233
|
+
'β' => 'β',
|
|
234
|
+
'γ' => 'γ',
|
|
235
|
+
'δ' => 'δ',
|
|
236
|
+
'ε' => 'ε',
|
|
237
|
+
'ζ' => 'ζ',
|
|
238
|
+
'η' => 'η',
|
|
239
|
+
'θ' => 'θ',
|
|
240
|
+
'ι' => 'ι',
|
|
241
|
+
'κ' => 'κ',
|
|
242
|
+
'λ' => 'λ',
|
|
243
|
+
'μ' => 'μ',
|
|
244
|
+
'ν' => 'ν',
|
|
245
|
+
'ξ' => 'ξ',
|
|
246
|
+
'ο' => 'ο',
|
|
247
|
+
'π' => 'π',
|
|
248
|
+
'ρ' => 'ρ',
|
|
249
|
+
'ς' => 'ς',
|
|
250
|
+
'σ' => 'σ',
|
|
251
|
+
'τ' => 'τ',
|
|
252
|
+
'υ' => 'υ',
|
|
253
|
+
'φ' => 'φ',
|
|
254
|
+
'χ' => 'χ',
|
|
255
|
+
'ψ' => 'ψ',
|
|
256
|
+
'ω' => 'ω',
|
|
257
|
+
'ϑ' => 'ϑ',
|
|
258
|
+
'ϒ' => 'ϒ',
|
|
259
|
+
'ϖ' => 'ϖ',
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
SPECIAL_HTML_CHARACTERS_REGEX = /&[a-zA-Z]+[0-9]*;/
|
|
263
|
+
|
|
264
|
+
def initialize(include_links=true)
|
|
265
|
+
@parsed_text = ''
|
|
266
|
+
@discard = false
|
|
267
|
+
@current_element = ''
|
|
268
|
+
@include_links = include_links
|
|
269
|
+
|
|
270
|
+
@current_list_index = -1
|
|
271
|
+
@lists = []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@table_columns = 0
|
|
275
|
+
@parsing_link = false
|
|
276
|
+
@link_text = ""
|
|
277
|
+
@link_href = ""
|
|
278
|
+
@parsing_bolds_and_italics = []
|
|
279
|
+
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def convert_special_characters(text)
|
|
283
|
+
|
|
284
|
+
special_characters = text.scan(SPECIAL_HTML_CHARACTERS_REGEX).uniq
|
|
285
|
+
|
|
286
|
+
if special_characters.any?
|
|
287
|
+
special_characters.each do |special_character|
|
|
288
|
+
if HTML_SPECIAL_CHARACTERS.key?(special_character)
|
|
289
|
+
text.gsub!(special_character, HTML_SPECIAL_CHARACTERS[special_character])
|
|
290
|
+
elsif HTML_SPECIAL_ESCAPED_CHARACTERS.key?(special_character)
|
|
291
|
+
text.gsub!(special_character, "\\#{HTML_SPECIAL_ESCAPED_CHARACTERS[special_character]}")
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
text
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def add_to_parsed_text(text)
|
|
300
|
+
if @parsing_link
|
|
301
|
+
@link_text << text
|
|
302
|
+
else
|
|
303
|
+
@parsed_text << convert_special_characters(text)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def start_element(name)
|
|
308
|
+
|
|
309
|
+
if name == :b || name == :strong
|
|
310
|
+
add_to_parsed_text("**")
|
|
311
|
+
@parsing_bolds_and_italics << :b
|
|
312
|
+
elsif name == :i || name == :em
|
|
313
|
+
add_to_parsed_text("*")
|
|
314
|
+
@parsing_bolds_and_italics << :i
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
if name == :script || name == :style || name == :head || name == :meta || name == :title
|
|
318
|
+
@discard = true
|
|
319
|
+
else
|
|
320
|
+
@discard = false
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
@current_element = name
|
|
324
|
+
|
|
325
|
+
if name== :hr
|
|
326
|
+
add_to_parsed_text("\n-------------------------------------------------------------------------------------------------------------------------------------------------\n")
|
|
327
|
+
end
|
|
328
|
+
if name == :ul || name == :ol
|
|
329
|
+
add_to_parsed_text("\n")
|
|
330
|
+
@lists << {
|
|
331
|
+
list_type: name,
|
|
332
|
+
ordered_list_level: 1,
|
|
333
|
+
ordered_list_type: "1", #If its an unordered list, this is not used.
|
|
334
|
+
}
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
if name.match(/h[1-6]/)
|
|
338
|
+
heading_number = name.to_s[1].to_i
|
|
339
|
+
add_to_parsed_text(("#" * heading_number) + " ")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
if name == :a && @include_links
|
|
343
|
+
@parsing_link = true
|
|
344
|
+
@link_text = ""
|
|
345
|
+
@link_href = ""
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def end_element(name)
|
|
350
|
+
@discard = false
|
|
351
|
+
|
|
352
|
+
if name == :p || name == :div || name.match(/h[1-6]/) || name == :br || name == :hr || name == :table || name == :tr
|
|
353
|
+
add_to_parsed_text("\n")
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
if name == :a && @include_links
|
|
357
|
+
@parsing_link = false
|
|
358
|
+
|
|
359
|
+
if @link_text.present? && @link_href.present?
|
|
360
|
+
add_to_parsed_text("[#{@link_text}](#{@link_href})")
|
|
361
|
+
elsif @link_href.present?
|
|
362
|
+
add_to_parsed_text("<#{@link_href}>")
|
|
363
|
+
elsif @link_text.present?
|
|
364
|
+
add_to_parsed_text(@link_text)
|
|
365
|
+
end
|
|
366
|
+
@link_text = ""
|
|
367
|
+
@link_href = ""
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
if name == :li
|
|
371
|
+
add_to_parsed_text("\n")
|
|
372
|
+
if @lists.last.present? && @lists.last[:list_type] == :ol
|
|
373
|
+
@lists.last[:ordered_list_level] += 1
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
if name == :ul || name == :ol
|
|
378
|
+
@lists.pop
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
if name == :b || name == :strong
|
|
382
|
+
#Find the first non space character backwards to check if the bold markers are empty or not.
|
|
383
|
+
last_non_space_index = @parsed_text.rindex(/\S/)
|
|
384
|
+
if last_non_space_index && last_non_space_index > 2 && @parsed_text[(last_non_space_index - 1)..last_non_space_index] == "**"
|
|
385
|
+
# Remove the empty bold markers from the parsed text.
|
|
386
|
+
@parsed_text = @parsed_text[0..(last_non_space_index - 2)]
|
|
387
|
+
else
|
|
388
|
+
#if last char is a space, we move the closing bold marker before the space to avoid having markdown syntax separated by spaces,
|
|
389
|
+
#which can cause issues in some markdown renderers. Same for italics.
|
|
390
|
+
if @parsed_text[-1] == " "
|
|
391
|
+
@parsed_text.rstrip!
|
|
392
|
+
add_to_parsed_text("**")
|
|
393
|
+
@parsed_text << " "
|
|
394
|
+
else
|
|
395
|
+
add_to_parsed_text("**")
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
elsif name == :i || name == :em
|
|
400
|
+
#Find the first non space character backwards to check if the italic markers are empty
|
|
401
|
+
last_non_space_index = @parsed_text.rindex(/\S/)
|
|
402
|
+
if last_non_space_index && last_non_space_index > 2 && @parsed_text[last_non_space_index] == "*"
|
|
403
|
+
# Remove the empty italic markers from the parsed text.
|
|
404
|
+
@parsed_text = @parsed_text[0..(last_non_space_index - 1)]
|
|
405
|
+
else
|
|
406
|
+
if @parsed_text[-1] == " "
|
|
407
|
+
@parsed_text.rstrip!
|
|
408
|
+
add_to_parsed_text("*")
|
|
409
|
+
@parsed_text << " "
|
|
410
|
+
else
|
|
411
|
+
add_to_parsed_text("*")
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def attr(name, value)
|
|
418
|
+
if (@include_links && @current_element == :a && name == :href)
|
|
419
|
+
@link_href << "#{value}"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
if (@current_element == :div && name == :style && value.match?(/border-\w*:\s*solid/))
|
|
423
|
+
add_to_parsed_text("\n-------------------------------------------------------------------------------------------------------------------------------------------------\n")
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
if (@current_element == :ol && name == :start && @lists.last.present?)
|
|
427
|
+
@lists.last[:ordered_list_level] = value.to_i
|
|
428
|
+
end
|
|
429
|
+
if (@current_element == :ol && name == :type && @lists.last.present?)
|
|
430
|
+
@lists.last[:ordered_list_type] = value
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def attrs_done()
|
|
435
|
+
if @current_element == :li
|
|
436
|
+
current_list = @lists.last
|
|
437
|
+
if current_list.nil?
|
|
438
|
+
#sometimes we encounter a <li> outside of a list, so we just ignore it
|
|
439
|
+
return
|
|
440
|
+
end
|
|
441
|
+
if @lists.size > 1
|
|
442
|
+
# We are inside a nested list, so we need to indent
|
|
443
|
+
add_to_parsed_text(" " * 2 * (@lists.size - 1))
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
if current_list[:list_type] == :ol
|
|
447
|
+
if current_list[:ordered_list_type] == "A"
|
|
448
|
+
add_to_parsed_text("#{('A'.ord + current_list[:ordered_list_level] - 1).chr}. ")
|
|
449
|
+
elsif current_list[:ordered_list_type] == "a"
|
|
450
|
+
add_to_parsed_text("#{('a'.ord + current_list[:ordered_list_level] - 1).chr}. ")
|
|
451
|
+
elsif current_list[:ordered_list_type] == "I"
|
|
452
|
+
add_to_parsed_text("#{roman_numeral(current_list[:ordered_list_level])}. ")
|
|
453
|
+
elsif current_list[:ordered_list_type] == "i"
|
|
454
|
+
add_to_parsed_text("#{roman_numeral(current_list[:ordered_list_level]).downcase}. ")
|
|
455
|
+
else
|
|
456
|
+
add_to_parsed_text("#{current_list[:ordered_list_level]}. ")
|
|
457
|
+
end
|
|
458
|
+
else
|
|
459
|
+
add_to_parsed_text("* ")
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def text(value)
|
|
466
|
+
|
|
467
|
+
unless value.blank? || @discard
|
|
468
|
+
if value.include?("-----------------------------")
|
|
469
|
+
add_to_parsed_text("\n-------------------------------------------------------------------------------------------------------------------------------------------------\n")
|
|
470
|
+
else
|
|
471
|
+
add_to_parsed_text("#{value}")
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def postprocess
|
|
477
|
+
|
|
478
|
+
# remove linefeeds (\r\n and \r -> \n)
|
|
479
|
+
@parsed_text.gsub!(/\r\n?/, "\n")
|
|
480
|
+
|
|
481
|
+
# strip extra spaces
|
|
482
|
+
@parsed_text.gsub!(/[ \t]*\302\240+[ \t]*/, " ") # non-breaking spaces -> spaces
|
|
483
|
+
@parsed_text.gsub!(/\n[\t]+/, "\n") # space at start of lines
|
|
484
|
+
@parsed_text.gsub!(/[ \t]+\n/, "\n") # space at end of lines
|
|
485
|
+
|
|
486
|
+
# no more than two consecutive newlines
|
|
487
|
+
@parsed_text.gsub!(/[\n]{3,}/, "\n\n")
|
|
488
|
+
|
|
489
|
+
@parsed_text.strip
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def roman_numeral(number)
|
|
493
|
+
roman_mapping = {
|
|
494
|
+
1000 => "M", 900 => "CM", 500 => "D", 400 => "CD", 100 => "C",
|
|
495
|
+
90 => "XC", 50 => "L", 40 => "XL", 10 => "X", 9 => "IX",
|
|
496
|
+
5 => "V", 4 => "IV", 1 => "I"
|
|
497
|
+
}
|
|
498
|
+
result = ""
|
|
499
|
+
roman_mapping.each do |value, letter|
|
|
500
|
+
result << letter * (number / value)
|
|
501
|
+
number = number % value
|
|
502
|
+
end
|
|
503
|
+
result
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
@@ -97,6 +97,13 @@ module EmailSignatureParser
|
|
|
97
97
|
MEETING_DOMAINS.any? { |regex| email_body =~ regex }
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
def remove_bold_and_header_markings(text)
|
|
101
|
+
return "" if text.nil? || text.empty?
|
|
102
|
+
text = text.gsub("*", "") # remove bold markings
|
|
103
|
+
text = text.gsub(/^#+\s*/, "") # remove header markings
|
|
104
|
+
text
|
|
105
|
+
end
|
|
106
|
+
|
|
100
107
|
def get_signature_sentences(name, email_address, email_body)
|
|
101
108
|
|
|
102
109
|
if email_body.nil? || email_body.empty?
|
|
@@ -383,7 +390,7 @@ module EmailSignatureParser
|
|
|
383
390
|
#Replace multiple commas with a single comma. or a comma preceded by a space
|
|
384
391
|
address_text = address_text.gsub(/\s+/, ' ').gsub(/,+|\ ,/, ',')
|
|
385
392
|
|
|
386
|
-
return address_text.strip
|
|
393
|
+
return remove_bold_and_header_markings(address_text).strip
|
|
387
394
|
end
|
|
388
395
|
|
|
389
396
|
def get_phones
|
|
@@ -541,6 +548,8 @@ module EmailSignatureParser
|
|
|
541
548
|
next
|
|
542
549
|
end
|
|
543
550
|
|
|
551
|
+
text = remove_bold_and_header_markings(text)
|
|
552
|
+
|
|
544
553
|
sections = text.split(',')
|
|
545
554
|
|
|
546
555
|
sections.each do |section|
|
|
@@ -619,7 +628,7 @@ module EmailSignatureParser
|
|
|
619
628
|
end
|
|
620
629
|
end
|
|
621
630
|
if job_titles_found.size > 0
|
|
622
|
-
parsed_titles << section.gsub(/\s+/, ' ').strip
|
|
631
|
+
parsed_titles << section.gsub(/\s+/, ' ').gsub('*', '').strip
|
|
623
632
|
end
|
|
624
633
|
end
|
|
625
634
|
end
|
|
@@ -659,8 +668,10 @@ module EmailSignatureParser
|
|
|
659
668
|
end
|
|
660
669
|
end
|
|
661
670
|
|
|
671
|
+
|
|
672
|
+
|
|
662
673
|
parsed_data = {
|
|
663
|
-
name: parsed_name,
|
|
674
|
+
name: remove_bold_and_header_markings(parsed_name),
|
|
664
675
|
email_address: email_address,
|
|
665
676
|
address: address,
|
|
666
677
|
phones: phones,
|
|
@@ -719,9 +730,10 @@ module EmailSignatureParser
|
|
|
719
730
|
smart: true,
|
|
720
731
|
mode: :generic,
|
|
721
732
|
effort: :tolerant,
|
|
733
|
+
convert_special: false,
|
|
722
734
|
}
|
|
723
735
|
input = StringIO.new(content)
|
|
724
|
-
text_handler =
|
|
736
|
+
text_handler = HtmlToMarkdownParser.new()
|
|
725
737
|
|
|
726
738
|
Ox.sax_html(text_handler, input, options)
|
|
727
739
|
text_handler.postprocess
|
|
@@ -843,7 +855,11 @@ module EmailSignatureParser
|
|
|
843
855
|
unless !email_html_body.nil? && !email_html_body.empty?
|
|
844
856
|
raise InvalidEmailError, "No email body provided"
|
|
845
857
|
end
|
|
846
|
-
|
|
858
|
+
|
|
859
|
+
if from.downcase.include?("reply") || from.downcase.include?("mailer-daemon")
|
|
860
|
+
raise InvalidFromError, "No valid From address"
|
|
861
|
+
end
|
|
862
|
+
|
|
847
863
|
|
|
848
864
|
name, email_address = extract_name_and_email(from)
|
|
849
865
|
parsed_text = parse_email_html(email_html_body)
|
|
@@ -862,6 +878,10 @@ module EmailSignatureParser
|
|
|
862
878
|
raise InvalidFromError, "No from provided"
|
|
863
879
|
end
|
|
864
880
|
|
|
881
|
+
if from.downcase.include?("reply") || from.downcase.include?("mailer-daemon")
|
|
882
|
+
raise InvalidFromError, "No valid From address"
|
|
883
|
+
end
|
|
884
|
+
|
|
865
885
|
if meeting_email?(email_body)
|
|
866
886
|
raise InvalidEmailError, "Email appears to be a meeting invite"
|
|
867
887
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: email_signature_parser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Guillermo Molini
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: ruby_postal
|
|
@@ -204,14 +203,13 @@ files:
|
|
|
204
203
|
- lib/email_signature_parser/data/job_titles/acronyms.yaml
|
|
205
204
|
- lib/email_signature_parser/data/job_titles/titles.yaml
|
|
206
205
|
- lib/email_signature_parser/data/job_titles/titles_es.yaml
|
|
207
|
-
- lib/email_signature_parser/
|
|
206
|
+
- lib/email_signature_parser/html_to_markdown_parser.rb
|
|
208
207
|
- lib/email_signature_parser/parser.rb
|
|
209
208
|
- lib/email_signature_parser/utils.rb
|
|
210
209
|
homepage: https://github.com/GMolini/email_signature_parser
|
|
211
210
|
licenses:
|
|
212
211
|
- MIT
|
|
213
212
|
metadata: {}
|
|
214
|
-
post_install_message:
|
|
215
213
|
rdoc_options: []
|
|
216
214
|
require_paths:
|
|
217
215
|
- lib
|
|
@@ -226,8 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
226
224
|
- !ruby/object:Gem::Version
|
|
227
225
|
version: '0'
|
|
228
226
|
requirements: []
|
|
229
|
-
rubygems_version:
|
|
230
|
-
signing_key:
|
|
227
|
+
rubygems_version: 4.0.15
|
|
231
228
|
specification_version: 4
|
|
232
229
|
summary: Email Signature Parser
|
|
233
230
|
test_files: []
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
module EmailSignatureParser
|
|
2
|
-
class HtmlTextParser < ::Ox::Sax
|
|
3
|
-
attr_reader :parsed_text
|
|
4
|
-
|
|
5
|
-
def initialize()
|
|
6
|
-
@parsed_text = ''
|
|
7
|
-
@discard = false
|
|
8
|
-
@current_element = ''
|
|
9
|
-
|
|
10
|
-
@current_list_index = -1
|
|
11
|
-
@lists = []
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@table_columns = 0
|
|
15
|
-
@parsing_link = false
|
|
16
|
-
@link_text = ""
|
|
17
|
-
@link_href = ""
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def start_element(name)
|
|
21
|
-
|
|
22
|
-
if name == :b || name == :strong
|
|
23
|
-
@parsed_text << " "
|
|
24
|
-
elsif name == :i || name == :em
|
|
25
|
-
@parsed_text << " "
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
if name == :script || name == :style || name == :head || name == :meta || name == :title
|
|
29
|
-
@discard = true
|
|
30
|
-
else
|
|
31
|
-
@discard = false
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
@current_element = name
|
|
35
|
-
if name == :br
|
|
36
|
-
@parsed_text << "\n"
|
|
37
|
-
end
|
|
38
|
-
if name== :hr
|
|
39
|
-
@parsed_text << "\n-------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
|
40
|
-
end
|
|
41
|
-
if name == :ul || name == :ol
|
|
42
|
-
@parsed_text << "\n"
|
|
43
|
-
@lists << {
|
|
44
|
-
list_type: name,
|
|
45
|
-
ordered_list_level: 1,
|
|
46
|
-
ordered_list_type: "1", #If its an unordered list, this is not used.
|
|
47
|
-
}
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
if name == :a
|
|
51
|
-
@parsing_link = true
|
|
52
|
-
@link_text = ""
|
|
53
|
-
@link_href = ""
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def end_element(name)
|
|
58
|
-
@discard = false
|
|
59
|
-
|
|
60
|
-
if name == :b || name == :strong
|
|
61
|
-
@parsed_text << " "
|
|
62
|
-
elsif name == :i || name == :em
|
|
63
|
-
@parsed_text << " "
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
if name == :p || name == :div || name.match(/h[1-6]/) || name == :br || name == :hr || name == :table || name == :tr
|
|
67
|
-
@parsed_text << "\n"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
if name == :a
|
|
71
|
-
@parsed_text << "<a href=\"#{@link_href}\">#{@link_text}</a>" #leave links in html format
|
|
72
|
-
@parsing_link = false
|
|
73
|
-
@link_text = ""
|
|
74
|
-
@link_href = ""
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
if name == :li
|
|
78
|
-
@parsed_text << "\n"
|
|
79
|
-
if @lists.last && @lists.last[:list_type] == :ol
|
|
80
|
-
@lists.last[:ordered_list_level] += 1
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
if name == :ul || name == :ol
|
|
85
|
-
@lists.pop
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def attr(name, value)
|
|
90
|
-
|
|
91
|
-
if (@current_element == :a && name == :href)
|
|
92
|
-
@link_href << value
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
if (@current_element == :div && name == :style && value.match?(/border-\w*:\s*solid/))
|
|
96
|
-
@parsed_text << "\n-------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
if (@current_element == :ol && name == :start && !@lists.last.nil?)
|
|
100
|
-
@lists.last[:ordered_list_level] = value.to_i
|
|
101
|
-
end
|
|
102
|
-
if (@current_element == :ol && name == :type && !@lists.last.nil?)
|
|
103
|
-
@lists.last[:ordered_list_type] = value
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def attrs_done()
|
|
108
|
-
if @current_element == :li
|
|
109
|
-
current_list = @lists.last
|
|
110
|
-
if current_list.nil?
|
|
111
|
-
#sometimes we encounter a <li> outside of a list, so we just ignore it
|
|
112
|
-
return
|
|
113
|
-
end
|
|
114
|
-
if @lists.size > 1
|
|
115
|
-
# We are inside a nested list, so we need to indent
|
|
116
|
-
@parsed_text << " " * 2 * (@lists.size - 1)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
if current_list[:list_type] == :ol
|
|
120
|
-
if current_list[:ordered_list_type] == "A"
|
|
121
|
-
@parsed_text << "#{('A'.ord + current_list[:ordered_list_level] - 1).chr}. "
|
|
122
|
-
elsif current_list[:ordered_list_type] == "a"
|
|
123
|
-
@parsed_text << "#{('a'.ord + current_list[:ordered_list_level] - 1).chr}. "
|
|
124
|
-
elsif current_list[:ordered_list_type] == "I"
|
|
125
|
-
@parsed_text << "#{roman_numeral(current_list[:ordered_list_level])}. "
|
|
126
|
-
elsif current_list[:ordered_list_type] == "i"
|
|
127
|
-
@parsed_text << "#{roman_numeral(current_list[:ordered_list_level]).downcase}. "
|
|
128
|
-
else
|
|
129
|
-
@parsed_text << "#{current_list[:ordered_list_level]}. "
|
|
130
|
-
end
|
|
131
|
-
else
|
|
132
|
-
@parsed_text << "* "
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def text(value)
|
|
139
|
-
if @parsing_link
|
|
140
|
-
@link_text << value
|
|
141
|
-
return
|
|
142
|
-
end
|
|
143
|
-
unless value.nil? || value.empty? || @discard
|
|
144
|
-
if value.include?("-----------------------------")
|
|
145
|
-
@parsed_text << "\n-------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
|
146
|
-
else
|
|
147
|
-
@parsed_text << value
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def postprocess()
|
|
153
|
-
|
|
154
|
-
# remove linefeeds (\r\n and \r -> \n)
|
|
155
|
-
@parsed_text.gsub!(/\r\n?/, "\n")
|
|
156
|
-
|
|
157
|
-
# strip extra spaces
|
|
158
|
-
@parsed_text.gsub!(/[ \t]*\302\240+[ \t]*/, " ") # non-breaking spaces -> spaces
|
|
159
|
-
@parsed_text.gsub!(/\n[\t]+/, "\n") # space at start of lines
|
|
160
|
-
@parsed_text.gsub!(/[ \t]+\n/, "\n") # space at end of lines
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@parsed_text = @parsed_text.split("\n").map(&:strip).join("\n") # strip lines
|
|
164
|
-
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def roman_numeral(number)
|
|
168
|
-
roman_mapping = {
|
|
169
|
-
1000 => "M", 900 => "CM", 500 => "D", 400 => "CD", 100 => "C",
|
|
170
|
-
90 => "XC", 50 => "L", 40 => "XL", 10 => "X", 9 => "IX",
|
|
171
|
-
5 => "V", 4 => "IV", 1 => "I"
|
|
172
|
-
}
|
|
173
|
-
result = ""
|
|
174
|
-
roman_mapping.each do |value, letter|
|
|
175
|
-
result << letter * (number / value)
|
|
176
|
-
number = number % value
|
|
177
|
-
end
|
|
178
|
-
result
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|