maillinks 0.1.5 → 1.0
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/bin/maillinks +18 -13
- data/changes.txt +11 -0
- data/doc/html/maillinks.html +171 -422
- data/doc/man/maillinks.1.gz +0 -0
- data/doc/pdf/maillinks.pdf +0 -0
- data/doc/rst/maillinks.rst +32 -7
- data/lib/basic_logging.rb +249 -0
- data/lib/color_output.rb +102 -0
- data/lib/constants.rb +7 -12
- data/lib/maillinks.rb +86 -47
- data/maillinks.gemspec +18 -12
- metadata +23 -26
- data/lib/file_checking.rb +0 -88
- data/lib/log.conf +0 -53
- data/lib/logging.rb +0 -104
- data/lib/translating.rb +0 -89
- data/lib/translations +0 -24
data/doc/rst/maillinks.rst
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
Synopsis
|
|
9
9
|
========
|
|
10
|
-
maillinks [mail-file]
|
|
10
|
+
maillinks [mail-file]
|
|
11
|
+
|
|
12
|
+
maillinks [mail-file] <log level>
|
|
11
13
|
|
|
12
14
|
or
|
|
13
15
|
|
|
@@ -26,6 +28,11 @@ directives which are likely to be addressed to a remotely running application.
|
|
|
26
28
|
below the email-text. You can scrutinize the URL and find information about the
|
|
27
29
|
destination, before you engage your web-browser or other tool to follow a link.
|
|
28
30
|
|
|
31
|
+
*MailLinks* displays the result, together with the original HTML-mail in the
|
|
32
|
+
first Web-Browser that can be executed. At the time of this writing, the
|
|
33
|
+
possible Browsers are: *Dillo, W3M, Lynx, Brave-Browser, Firefox*. If none of
|
|
34
|
+
these is found, the program aborts with an error message.
|
|
35
|
+
|
|
29
36
|
Integration in mail-clients
|
|
30
37
|
----------------------------
|
|
31
38
|
|
|
@@ -46,8 +53,14 @@ macros or similar options can be configured.
|
|
|
46
53
|
|
|
47
54
|
Options
|
|
48
55
|
=========
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
When called with a mail file as first argument, *Maillinks* can interpret
|
|
57
|
+
as second argument a log level, to control the verboseness of the program
|
|
58
|
+
output. The default is *info*, but you can have more detailed console output
|
|
59
|
+
with *debug*.
|
|
60
|
+
|
|
61
|
+
When used as a filter, reading from *STDIN*, **no additional arguments are
|
|
62
|
+
accepted**.
|
|
63
|
+
|
|
51
64
|
|
|
52
65
|
Other Information
|
|
53
66
|
==================
|
|
@@ -61,14 +74,26 @@ Source-code and Development
|
|
|
61
74
|
the data-archive with *tar -xzf*.
|
|
62
75
|
|
|
63
76
|
Version
|
|
64
|
-
0.1
|
|
77
|
+
0.2.1 (April 2026)
|
|
78
|
+
|
|
79
|
+
.. _WTFPL: https://www.wtfpl.net/about/
|
|
65
80
|
|
|
66
81
|
License
|
|
67
|
-
*Maillinks* is published under the conditions of the
|
|
68
|
-
License, version 3.0 or later.
|
|
82
|
+
*Maillinks* is published under the conditions of the WTFPL_, version 2.0.
|
|
69
83
|
|
|
70
84
|
Author
|
|
71
85
|
Michael Uplawski <michael.uplawski@uplawski.eu>
|
|
72
86
|
|
|
73
|
-
|
|
87
|
+
This document
|
|
88
|
+
-------------
|
|
89
|
+
.. _CC By-ND 4.0: https://creativecommons.org/licenses/by-nd/4.0/
|
|
90
|
+
.. |date| date::
|
|
91
|
+
|
|
92
|
+
©Michael Uplawski <michael.uplawski@uplawski.eu>
|
|
93
|
+
|
|
94
|
+
License: `CC BY-ND 4.0`_
|
|
95
|
+
|
|
96
|
+
Date: |date|
|
|
97
|
+
|
|
98
|
+
**Ω**
|
|
74
99
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/bin/env ruby
|
|
2
|
+
#encoding: UTF-8
|
|
3
|
+
=begin
|
|
4
|
+
/***************************************************************************
|
|
5
|
+
* 2023-2026, Michael Uplawski <michael.uplawski@uplawski.eu> *
|
|
6
|
+
* This program is free software; you can redistribute it and/or modify *
|
|
7
|
+
* it under the terms of the WTFPL 2.0 or later, see *
|
|
8
|
+
* http://www.wtfpl.net/about/ *
|
|
9
|
+
* *
|
|
10
|
+
* This program is distributed in the hope that it will be useful, *
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
|
|
13
|
+
* *
|
|
14
|
+
***************************************************************************/
|
|
15
|
+
=end
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Simplified logging.
|
|
19
|
+
# See example code at the bottom of this file.
|
|
20
|
+
# Execute this file to see the output.
|
|
21
|
+
module BasicLogging
|
|
22
|
+
|
|
23
|
+
DEBUG = 0
|
|
24
|
+
INFO = 1
|
|
25
|
+
WARN = 2
|
|
26
|
+
ERROR = 3
|
|
27
|
+
FATAL = 4
|
|
28
|
+
UNKNOWN = nil
|
|
29
|
+
|
|
30
|
+
# this is mainly for the translation of method calls into log levels
|
|
31
|
+
Levels = {:debug => DEBUG, :info => INFO, :warn => WARN, :error => ERROR,
|
|
32
|
+
:fatal => FATAL, :unknown => UNKNOWN}
|
|
33
|
+
|
|
34
|
+
@@log_level = UNKNOWN
|
|
35
|
+
@@target = STDOUT
|
|
36
|
+
@@muted = []
|
|
37
|
+
|
|
38
|
+
# do not log, if caller is obj (class or instance)
|
|
39
|
+
def self.mute(obj)
|
|
40
|
+
name = obj.class == Class ? obj.name.dup : obj.class.name
|
|
41
|
+
@@muted << name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.is_muted?(obj)
|
|
45
|
+
name = obj.class == Class ? obj.name.dup : obj.class.name
|
|
46
|
+
@@muted.include?(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# set the log level
|
|
50
|
+
def set_level(lv)
|
|
51
|
+
if lv.respond_to?(:to_str) && Levels.keys.include?(lv.strip.to_sym)
|
|
52
|
+
lv = Levels[lv.to_sym]
|
|
53
|
+
elsif lv.respond_to?(:to_sym) && Levels.keys.include?(lv)
|
|
54
|
+
lv = Levels[lv]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if(!lv || (lv.respond_to?(:to_int) && lv >= DEBUG && lv <= FATAL) )
|
|
58
|
+
@@log_level = lv
|
|
59
|
+
else
|
|
60
|
+
msg = calling_method << ": ERROR : invalid log level \"" << lv.to_s << "\""
|
|
61
|
+
msg << "\n" << "Keepinng old log level " << Levels.keys.detect {| k| Levels[k] == @@log_level}.to_s
|
|
62
|
+
STDERR.puts msg
|
|
63
|
+
puts msg
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# set the log target
|
|
68
|
+
def set_target(tg)
|
|
69
|
+
if tg.respond_to?(:to_io)
|
|
70
|
+
@@target = tg
|
|
71
|
+
elsif(!File::exist?(tg) || ( File.file?(tg) && File.writable?(tg) ) )
|
|
72
|
+
@@target = File.open(tg, 'w+')
|
|
73
|
+
elsif !tg || tg.respond_to?(:to_str) && tg.strip.empty?
|
|
74
|
+
@@target = nil
|
|
75
|
+
else
|
|
76
|
+
STDERR.puts calling_method << ': ERROR : target ' << tg.to_str << ' cannot be set'
|
|
77
|
+
STDERR.puts "Keeping old target " << @@target.inspect
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Output of log messages, depending on the set log level and the name of the
|
|
83
|
+
# alias method which is actually called.
|
|
84
|
+
def log(message)
|
|
85
|
+
if !BasicLogging.is_muted?(self)
|
|
86
|
+
# how has this method been called?
|
|
87
|
+
mlevel = __callee__
|
|
88
|
+
if Levels.has_key?(mlevel) && Levels[mlevel] <= FATAL
|
|
89
|
+
# output only for levels equal or above the value that corresponds to
|
|
90
|
+
# the calling alias.
|
|
91
|
+
format_log( message, mlevel) if @@log_level && Levels[mlevel] >= @@log_level
|
|
92
|
+
else
|
|
93
|
+
STDERR.puts calling_method << ": ERROR : invalid log level \"" << mlevel.to_s << "\""
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def target
|
|
99
|
+
@@target.path if @@target
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def level
|
|
103
|
+
@@level.to_s if @@level
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Clear the log (-file)
|
|
107
|
+
def clear_log
|
|
108
|
+
if @@target && @@target.respond_to?(:truncate)
|
|
109
|
+
lock_target{ @@target.truncate(0) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# find the name of the calling object's class or the file name in case of the
|
|
114
|
+
# top level object... or any dumb instance of Object
|
|
115
|
+
def log_name()
|
|
116
|
+
if !@log_name
|
|
117
|
+
clname = self.class.name
|
|
118
|
+
# limit analysis of the call stack to 'Object'.
|
|
119
|
+
if clname == 'Object'
|
|
120
|
+
@log_name = File::basename(caller.last.split(':')[0])
|
|
121
|
+
else
|
|
122
|
+
@log_name = self.class == Class ? self.name.dup << ' [class]' : clname
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
@log_name
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
alias :debug :log
|
|
129
|
+
alias :info :log
|
|
130
|
+
alias :warn :log
|
|
131
|
+
alias :error :log
|
|
132
|
+
alias :fatal :log
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Find the method that called, or 'main' in case of a top level Object.
|
|
137
|
+
# This needs a bunch of memory and slows down the process.
|
|
138
|
+
# Use of this method should be limited.
|
|
139
|
+
def calling_method()
|
|
140
|
+
# find instances other than the current module.
|
|
141
|
+
stack_item = caller.detect do |item|
|
|
142
|
+
item.split(':')[0] != __FILE__
|
|
143
|
+
end
|
|
144
|
+
if stack_item
|
|
145
|
+
if stack_item.include?('#')
|
|
146
|
+
m = stack_item.split(':').last.split("#")[1]
|
|
147
|
+
m.gsub!("'", '') if m && m.include?("'")
|
|
148
|
+
# looks cooler with a class name in front.
|
|
149
|
+
return '#' << m << '()'
|
|
150
|
+
else
|
|
151
|
+
# top level Object
|
|
152
|
+
return stack_item.split(':').last
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
# This is dumb but won't hurt a lot.
|
|
156
|
+
return 'from main'
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# could be useful
|
|
161
|
+
def lock_target(&block)
|
|
162
|
+
begin
|
|
163
|
+
if @@target.respond_to?(:flock)
|
|
164
|
+
@@target.flock(File::LOCK_EX)
|
|
165
|
+
block.call
|
|
166
|
+
@@target.flock(File::LOCK_UN)
|
|
167
|
+
elsif @@target.respond_to?(:to_io)
|
|
168
|
+
block.call
|
|
169
|
+
end
|
|
170
|
+
rescue => ex
|
|
171
|
+
STDERR.puts calling_method << ": ERROR : cannot lock target (" << ex.message << ")"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# 1 format_log for all loggers.
|
|
176
|
+
def format_log(message, mlevel)
|
|
177
|
+
if @@target
|
|
178
|
+
location = log_name().dup << ' '
|
|
179
|
+
# Limit the memory-hungry analysis of the call stack
|
|
180
|
+
# This also takes about 2.5 times as long.
|
|
181
|
+
if mlevel != :info
|
|
182
|
+
location = log_name().dup << ' ' << calling_method << ' '
|
|
183
|
+
end
|
|
184
|
+
# indicate if a registered class or the registered object of a class is calling.
|
|
185
|
+
lock_target{@@target.puts '' << location << mlevel.to_s << ' ' << Time.now.strftime("%H:%M:%S:%6N") << ': ' << message.gsub("\n", "\n |")}
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end # Module end
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
#---------test: execute file----------
|
|
193
|
+
if $0 == __FILE__
|
|
194
|
+
Array.extend(BasicLogging)
|
|
195
|
+
Array.set_level(BasicLogging::INFO)
|
|
196
|
+
Array.info('TEST')
|
|
197
|
+
ar = Array.new
|
|
198
|
+
ar.extend(BasicLogging)
|
|
199
|
+
# --- no output :
|
|
200
|
+
l = __LINE__
|
|
201
|
+
ar.debug(l.next.to_s << ': debug-test 0')
|
|
202
|
+
# output
|
|
203
|
+
ar.set_level(BasicLogging::DEBUG)
|
|
204
|
+
l = __LINE__
|
|
205
|
+
ar.debug(l.next.to_s << ': debug-test 1')
|
|
206
|
+
|
|
207
|
+
obj = Object.new
|
|
208
|
+
obj.extend(BasicLogging)
|
|
209
|
+
obj.set_level(BasicLogging::DEBUG)
|
|
210
|
+
puts "--------debug-----------"
|
|
211
|
+
obj.debug('debug')
|
|
212
|
+
obj.info('info')
|
|
213
|
+
obj.warn('warn')
|
|
214
|
+
obj.error('error')
|
|
215
|
+
obj.fatal('fatal')
|
|
216
|
+
puts "--------info-----------"
|
|
217
|
+
obj.set_level("info")
|
|
218
|
+
obj.debug('debug')
|
|
219
|
+
obj.info('info')
|
|
220
|
+
obj.warn('warn')
|
|
221
|
+
obj.error('error')
|
|
222
|
+
obj.fatal('fatal')
|
|
223
|
+
puts "--------fatal-----------"
|
|
224
|
+
obj.set_level("fatal")
|
|
225
|
+
obj.debug('debug')
|
|
226
|
+
obj.info('info')
|
|
227
|
+
obj.warn('warn')
|
|
228
|
+
obj.error('error')
|
|
229
|
+
obj.fatal('fatal')
|
|
230
|
+
puts "--------UNKNOWN-----------"
|
|
231
|
+
obj.set_level(nil)
|
|
232
|
+
obj.debug('debug')
|
|
233
|
+
obj.info('info')
|
|
234
|
+
obj.warn('warn')
|
|
235
|
+
obj.error('error')
|
|
236
|
+
obj.fatal('fatal')
|
|
237
|
+
# ---- into file ---
|
|
238
|
+
obj.set_target "/tmp/test_log.log"
|
|
239
|
+
puts "------ INFO into #{obj.target}-----------"
|
|
240
|
+
obj.set_level BasicLogging::INFO
|
|
241
|
+
obj.info('info output')
|
|
242
|
+
|
|
243
|
+
obj.info('info output 2')
|
|
244
|
+
puts "---------- invalid (fails) -------"
|
|
245
|
+
obj.set_target "/dev/sr0"
|
|
246
|
+
obj.set_level "power"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# EOF
|
data/lib/color_output.rb
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#encoding: UTF-8
|
|
2
|
+
=begin
|
|
3
|
+
/***************************************************************************
|
|
4
|
+
* 2023-2024, Michael Uplawski <michael.uplawski@uplawski.eu> *
|
|
5
|
+
* This program is free software; you can redistribute it and/or modify *
|
|
6
|
+
* it under the terms of the WTFPL 2.0 or later, see *
|
|
7
|
+
* http://www.wtfpl.net/about/ *
|
|
8
|
+
* *
|
|
9
|
+
* This program is distributed in the hope that it will be useful, *
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
|
|
12
|
+
* *
|
|
13
|
+
***************************************************************************/
|
|
14
|
+
=end
|
|
15
|
+
|
|
16
|
+
# Functions to apply colors to terminal output.
|
|
17
|
+
# This is stolen from the Internet and I have lost track of my own additions
|
|
18
|
+
# and modifications.
|
|
19
|
+
|
|
20
|
+
COLORS = {:default => 9, :black => 0, :red => 1, :green => 2, :yellow => 3, :blue => 4, :purple => 5, :cyan => 6, :white => 7 }
|
|
21
|
+
|
|
22
|
+
BG = 4
|
|
23
|
+
FG = 3
|
|
24
|
+
REGULAR = 0
|
|
25
|
+
BOLD = 1
|
|
26
|
+
UNDERLINE = 4
|
|
27
|
+
BLINK = 5
|
|
28
|
+
SWAP = 7
|
|
29
|
+
NEUTRAL = 0
|
|
30
|
+
|
|
31
|
+
STYLES = {:regular => REGULAR, :bold => BOLD, :underline => UNDERLINE, :blink => BLINK, :swap => SWAP, :neutral => NEUTRAL}
|
|
32
|
+
|
|
33
|
+
# Colorizes the given text. Color-code is either an escape-sequence or one of
|
|
34
|
+
# the symbols representing color-names in the COLORS hash.
|
|
35
|
+
def colorize(text, color_code)
|
|
36
|
+
# ensure that colors are supported
|
|
37
|
+
if $stdout.tty? && ENV['TERM'] != 'dumb'
|
|
38
|
+
if (COLORS.keys.include?(color_code) )
|
|
39
|
+
"\033[3#{COLORS[color_code]}m#{text}\033[0m"
|
|
40
|
+
else
|
|
41
|
+
"#{color_code}#{text}\033[0m"
|
|
42
|
+
end
|
|
43
|
+
# if Terminal does not support colors
|
|
44
|
+
else
|
|
45
|
+
text
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# rgb foreground
|
|
50
|
+
def colorize_RGB(text, r, g, b)
|
|
51
|
+
code = "%s;%s;%s" %[r, g, b]
|
|
52
|
+
"\033[38;2;#{code}m#{text}\033[0m"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# rgb background
|
|
56
|
+
def background_RGB(text, r, g, b)
|
|
57
|
+
code = "%s;%s;%s" %[r, g, b]
|
|
58
|
+
"\033[48;2;#{code}m#{text}\033[0m"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# duplicate code without much intelligence
|
|
62
|
+
def style(text, style_code)
|
|
63
|
+
"#{style_code}#{text}\033[0m"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# a function which allows to manipulate every known aspect of the ansi-output.
|
|
67
|
+
def colored_output(output_text, fg_color = :default, bg_color = :default, style = :regular , mode = :neutral )
|
|
68
|
+
if $stdout.tty? && ENV['TERM'] != 'dumb'
|
|
69
|
+
"\033[%i;%i;%i%i;%i%im%s\033[0m" %[STYLES[mode.to_sym], STYLES[style.to_sym], FG, COLORS[fg_color.to_sym], BG, COLORS[bg_color.to_sym], output_text]
|
|
70
|
+
else
|
|
71
|
+
# fallback for terminals that do not support colors.
|
|
72
|
+
output_text
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# convenience functions
|
|
77
|
+
def black(text); colorize(text, "\033[30m"); end
|
|
78
|
+
def red(text); colorize(text, "\033[31m"); end
|
|
79
|
+
def green(text); colorize(text, "\033[32m"); end
|
|
80
|
+
def yellow(text); colorize(text, "\033[33m"); end
|
|
81
|
+
def purple(text); colorize(text, "\033[35m"); end
|
|
82
|
+
def cyan(text); colorize(text, "\033[36m"); end
|
|
83
|
+
def blue(text); colorize(text, "\033[34m"); end
|
|
84
|
+
def white(text); colorize(text, "\033[37m"); end
|
|
85
|
+
|
|
86
|
+
# RGB
|
|
87
|
+
# TODO: Fallbacks from the 256 color palette
|
|
88
|
+
def orange(text); colorize(text, "\033[38;2;255;165;0m"); end
|
|
89
|
+
def pink(text); colorize(text, "\033[38;2;255;0;165m"); end
|
|
90
|
+
def lightred(text); colorize(text, "\033[38;2;255;100;100m"); end
|
|
91
|
+
def lightgreen(text); colorize(text, "\033[38;2;100;255;100m"); end
|
|
92
|
+
def brown(text); colorize(text, "\033[38;2;153;76;0m"); end
|
|
93
|
+
def crimson(text); colorize(text, "\033[38;2;220;20;60m"); end
|
|
94
|
+
|
|
95
|
+
# quick contrast
|
|
96
|
+
def black_on_white(text); colorize(colorize(text, "\033[30m"), "\033[47m");end
|
|
97
|
+
def white_on_black(text); colorize(colorize(text, "\033[37m"), "\033[40m");end
|
|
98
|
+
|
|
99
|
+
# styles
|
|
100
|
+
def bold(text); style(text, "\033[01m");end
|
|
101
|
+
def underline(text); style(text, "\033[04m");end
|
|
102
|
+
|
data/lib/constants.rb
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
#encoding: UTF-8
|
|
2
2
|
=begin
|
|
3
3
|
/***************************************************************************
|
|
4
|
-
*
|
|
5
|
-
* *
|
|
4
|
+
* 2023-2026, Michael Uplawski <michael.uplawski@uplawski.eu> *
|
|
6
5
|
* This program is free software; you can redistribute it and/or modify *
|
|
7
|
-
* it under the terms of the
|
|
8
|
-
*
|
|
9
|
-
* (at your option) any later version. *
|
|
6
|
+
* it under the terms of the WTFPL 2.0 or later, see *
|
|
7
|
+
* http://www.wtfpl.net/about/ *
|
|
10
8
|
* *
|
|
11
9
|
* This program is distributed in the hope that it will be useful, *
|
|
12
10
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
13
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
14
|
-
* GNU General Public License for more details. *
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
|
|
15
12
|
* *
|
|
16
|
-
* You should have received a copy of the GNU General Public License *
|
|
17
|
-
* along with this program; if not, write to the *
|
|
18
|
-
* Free Software Foundation, Inc., *
|
|
19
|
-
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
|
20
13
|
***************************************************************************/
|
|
21
14
|
=end
|
|
22
15
|
|
|
@@ -26,5 +19,7 @@ require 'tmpdir'
|
|
|
26
19
|
# ignore the rules for HTML. Now they write HTML-mail.
|
|
27
20
|
Block_Tags = %w~body div p hr table img~
|
|
28
21
|
|
|
29
|
-
|
|
22
|
+
# use the first one available
|
|
23
|
+
BROWSERS = ['dillo', 'w3m', 'lynx', 'brave-browser', 'firefox']
|
|
24
|
+
|
|
30
25
|
OUT_FILE = "%s/mail_message.html" %Dir.tmpdir
|
data/lib/maillinks.rb
CHANGED
|
@@ -1,86 +1,125 @@
|
|
|
1
1
|
#encoding: UTF-8
|
|
2
2
|
=begin
|
|
3
3
|
/***************************************************************************
|
|
4
|
-
*
|
|
5
|
-
* *
|
|
4
|
+
* 2023-2026, Michael Uplawski <michael.uplawski@uplawski.eu> *
|
|
6
5
|
* This program is free software; you can redistribute it and/or modify *
|
|
7
|
-
* it under the terms of the
|
|
8
|
-
*
|
|
9
|
-
* (at your option) any later version. *
|
|
6
|
+
* it under the terms of the WTFPL 2.0 or later, see *
|
|
7
|
+
* http://www.wtfpl.net/about/ *
|
|
10
8
|
* *
|
|
11
9
|
* This program is distributed in the hope that it will be useful, *
|
|
12
10
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
13
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
14
|
-
* GNU General Public License for more details. *
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
|
|
15
12
|
* *
|
|
16
|
-
* You should have received a copy of the GNU General Public License *
|
|
17
|
-
* along with this program; if not, write to the *
|
|
18
|
-
* Free Software Foundation, Inc., *
|
|
19
|
-
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
|
20
13
|
***************************************************************************/
|
|
21
14
|
=end
|
|
22
15
|
|
|
23
|
-
require_relative '
|
|
24
|
-
require_relative 'logging'
|
|
16
|
+
require_relative 'basic_logging'
|
|
25
17
|
require_relative 'constants'
|
|
18
|
+
require_relative 'color_output'
|
|
26
19
|
require 'mail'
|
|
27
20
|
require 'nokogiri'
|
|
28
21
|
require 'escape'
|
|
29
22
|
|
|
30
23
|
# show me the urls.
|
|
31
24
|
class MailLinks
|
|
32
|
-
include
|
|
25
|
+
include BasicLogging
|
|
33
26
|
def initialize(mail = nil)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# a file may be piped in from the command-line...
|
|
37
|
-
@log.debug('parameter is ' << (mail ? mail : ' N I L' ))
|
|
27
|
+
# a file may be piped in from the command-line...
|
|
28
|
+
debug('parameter is ' << (mail ? mail : ' N I L' ))
|
|
38
29
|
if(mail && '-' != mail.strip)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@log.debug('will work on mail-file ' << mail)
|
|
46
|
-
@mail = mail
|
|
47
|
-
|
|
48
|
-
File::open(OUT_FILE, 'w+') {|of| of << File::read(@mail) }
|
|
30
|
+
[:exist?, :file?, :readable?].each do |cond|
|
|
31
|
+
if !File.send(cond, mail)
|
|
32
|
+
error(yellow("#{mail}: not #{cond.split('?')[0]}. Aborting!") )
|
|
33
|
+
usage
|
|
34
|
+
exit false
|
|
35
|
+
end
|
|
49
36
|
end
|
|
37
|
+
debug('will work on mail-file ' << mail)
|
|
38
|
+
@mail = mail
|
|
39
|
+
|
|
40
|
+
File::open(OUT_FILE, 'w+') {|of| of << File::read(@mail) }
|
|
50
41
|
elsif(!$<.eof?)
|
|
51
|
-
|
|
42
|
+
info('Reading data from STDIN. Hit Ctrl+D to interrupt')
|
|
52
43
|
input = $<.read
|
|
53
|
-
|
|
44
|
+
debug('input length is ' << input.length.to_s)
|
|
54
45
|
if(input.length < 10)
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
error(yellow('insufficient input data'))
|
|
47
|
+
error(yellow('No mail text provided'))
|
|
57
48
|
usage()
|
|
58
49
|
exit false
|
|
59
50
|
end
|
|
60
51
|
# ... provide a temporary file in that case,
|
|
61
|
-
# @mail = OUT_FILE
|
|
62
52
|
File.open(OUT_FILE, 'w+') {|of| of << input}
|
|
63
53
|
@mail = OUT_FILE.dup
|
|
64
|
-
|
|
54
|
+
debug('Temporary eml-file is written.')
|
|
65
55
|
end
|
|
66
56
|
|
|
67
57
|
procede
|
|
68
58
|
end
|
|
69
59
|
|
|
70
60
|
private
|
|
61
|
+
|
|
62
|
+
def browser()
|
|
63
|
+
path_dirs = ENV['PATH'].split(':')
|
|
64
|
+
BROWSERS.detect do |b|
|
|
65
|
+
debug 'trying to find browser: ' << b.to_s
|
|
66
|
+
# array of the directories in $PATH
|
|
67
|
+
|
|
68
|
+
path_dirs.each do |dir_name|
|
|
69
|
+
debug('looking in ' << dir_name)
|
|
70
|
+
return [b] if Dir.entries(dir_name).include?(b)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
# Look for yad, when everything else has failed.
|
|
74
|
+
path_dirs.each do |dir_name|
|
|
75
|
+
if Dir.entries(dir_name).include?('yad')
|
|
76
|
+
return ['yad', '--browser', '--html', '--width=1000', '--height=800', '--uri=']
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
error 'Cannot find any of the given browsers: ' << BROWSERS.join(', ') << ', yad'
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
def procede
|
|
72
84
|
handle_mail
|
|
73
85
|
show_mail
|
|
74
86
|
end
|
|
75
87
|
def usage
|
|
76
|
-
msg =
|
|
88
|
+
msg = "\nUsage: " << $0 << ' ' << '<mail-file>'
|
|
77
89
|
msg << "\n or: " << 'cat <mail-file> |' << $0
|
|
90
|
+
msg << "\nYou can name a log level as second parameter:\n"
|
|
91
|
+
msg << " " << $0 << ' ' << '<mail-file>' << ' ' << 'debug'
|
|
92
|
+
msg << "\nThis will create more verbose program output.\n"
|
|
78
93
|
puts msg
|
|
79
94
|
end
|
|
80
95
|
|
|
81
96
|
def show_mail
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
# prog is an array of 1 or more elements.
|
|
98
|
+
prog = browser()
|
|
99
|
+
debug 'browser is ' << (prog ? prog.to_s : ' N I L ')
|
|
100
|
+
if prog
|
|
101
|
+
uri_arg = OUT_FILE
|
|
102
|
+
if prog.length > 1
|
|
103
|
+
# concatenate out_file to the uri-parameter
|
|
104
|
+
uri_arg = prog.pop + OUT_FILE
|
|
105
|
+
end
|
|
106
|
+
# attach the uri_parameter, escape.
|
|
107
|
+
cmd = Escape.shell_command(prog.dup.push uri_arg)
|
|
108
|
+
debug 'command is ' << cmd
|
|
109
|
+
info cyan('<----------- Output from ' << prog[0] << '---------->')
|
|
110
|
+
begin
|
|
111
|
+
pid = Process.spawn(cmd)
|
|
112
|
+
Process.wait
|
|
113
|
+
info cyan('-----------> ' << prog[0] << ' END <----------')
|
|
114
|
+
debug 'removing temporary file ' << OUT_FILE
|
|
115
|
+
File.unlink(OUT_FILE)
|
|
116
|
+
rescue Exception => ex
|
|
117
|
+
error __LINE__.dup << ' ' << ex.message
|
|
118
|
+
end
|
|
119
|
+
else
|
|
120
|
+
fatal yellow('Cannot open browser!' + bold(' ABORTING.') )
|
|
121
|
+
exit false
|
|
122
|
+
end
|
|
84
123
|
end
|
|
85
124
|
|
|
86
125
|
def handle_mail
|
|
@@ -89,8 +128,8 @@ class MailLinks
|
|
|
89
128
|
mail_body = mail_content.split("</head>")[1].split("</html>")[0]
|
|
90
129
|
mail_body = Mail::Encodings::QuotedPrintable.decode(mail_body)
|
|
91
130
|
rescue Exception => ex
|
|
92
|
-
|
|
93
|
-
|
|
131
|
+
error('Cannot find the html-body in this mail. Maybe it is not a HTML-mail?')
|
|
132
|
+
debug(ex.message)
|
|
94
133
|
end
|
|
95
134
|
if !mail_body
|
|
96
135
|
mail_body = mail_content
|
|
@@ -100,7 +139,7 @@ class MailLinks
|
|
|
100
139
|
|
|
101
140
|
a_nodes = html_mail.xpath(".//a")
|
|
102
141
|
if(! a_nodes || a_nodes.empty? )
|
|
103
|
-
|
|
142
|
+
info(green('no links in this mail') )
|
|
104
143
|
exit true
|
|
105
144
|
end
|
|
106
145
|
begin
|
|
@@ -114,30 +153,30 @@ class MailLinks
|
|
|
114
153
|
body.add_child("<hr/>")
|
|
115
154
|
body.add_child("<h2>HTML-links in this mail</h2>")[0]
|
|
116
155
|
if(!body)
|
|
117
|
-
|
|
156
|
+
error('cannot find a body-tag in this mail')
|
|
118
157
|
exit false
|
|
119
158
|
end
|
|
120
159
|
dl = body.add_child("<dl id='links'></dl>")[0]
|
|
121
160
|
if(!dl)
|
|
122
|
-
|
|
161
|
+
error('cannot add a list-tag')
|
|
123
162
|
exit false
|
|
124
163
|
end
|
|
125
164
|
links_hash = Hash.new
|
|
126
165
|
a_nodes.each_with_index do |a, i|
|
|
127
|
-
|
|
166
|
+
debug('a is ' << a.to_s.gsub("=\" ", ''))
|
|
128
167
|
caption = a.inner_text.to_s.strip
|
|
129
168
|
href = a.attribute('href')
|
|
130
169
|
if(href)
|
|
131
|
-
|
|
170
|
+
debug('href is: ' << href.to_s)
|
|
132
171
|
link = href.content
|
|
133
|
-
|
|
172
|
+
debug('link is ex href.content: ' << link.to_s)
|
|
134
173
|
a.add_next_sibling("<span>[" << (i.next).to_s << "]</span>")
|
|
135
174
|
dl.add_child("<dt>%i) %s</dt>"%[i.next, caption])
|
|
136
|
-
dl.add_child("<dd style='white-space:nowrap;'><a href='%s'>%s</a></dd>"%[link,link])
|
|
175
|
+
dl.add_child("<dd style='white-space:nowrap;'><a style='white-space:nowrap;' href='%s'>%s</a></dd>"%[link,link])
|
|
137
176
|
end
|
|
138
177
|
end
|
|
139
178
|
rescue Exception => ex
|
|
140
|
-
|
|
179
|
+
error(yellow("cannot handle the HTML in this mail: ") << bold(ex.message) )
|
|
141
180
|
exit false
|
|
142
181
|
end
|
|
143
182
|
|