active_mailbox 0.0.1
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.
- data/LICENSE +20 -0
- data/README.markdown +215 -0
- data/Rakefile +33 -0
- data/bin/active_mailbox +6 -0
- data/lib/active_mailbox.rb +16 -0
- data/lib/active_mailbox/cli.rb +112 -0
- data/lib/active_mailbox/errors.rb +8 -0
- data/lib/active_mailbox/folder.rb +166 -0
- data/lib/active_mailbox/mailbox.rb +250 -0
- data/lib/active_mailbox/message.rb +136 -0
- data/lib/active_mailbox/version.rb +3 -0
- data/test/folder_test.rb +70 -0
- data/test/mailbox_test.rb +135 -0
- data/test/message_test.rb +50 -0
- data/test/test_helper.rb +97 -0
- metadata +104 -0
@@ -0,0 +1,250 @@
|
|
1
|
+
module ActiveMailbox
|
2
|
+
#
|
3
|
+
# The ActiveMailbox::Mailbox class represents a top-level Asterisk
|
4
|
+
# mailbox. A mailbox contains folders, which contain the actual
|
5
|
+
# voicemails.
|
6
|
+
#
|
7
|
+
class Mailbox
|
8
|
+
#
|
9
|
+
# Greetings that can be altered by ActiveMailbox
|
10
|
+
#
|
11
|
+
ValidGreetings = [:unavail, :temp, :greet]
|
12
|
+
|
13
|
+
include Comparable
|
14
|
+
|
15
|
+
class << self
|
16
|
+
#
|
17
|
+
# The Mailbox currently in use
|
18
|
+
#
|
19
|
+
attr_accessor :current_mailbox
|
20
|
+
|
21
|
+
#
|
22
|
+
# Find mailbox
|
23
|
+
#
|
24
|
+
# On a default Asterisk installation, this would be
|
25
|
+
# /var/spool/asterisk/voicemail/CONTEXT/MAILBOX
|
26
|
+
#
|
27
|
+
# Note: if context is not provided, it will be set to mailbox[1, 3],
|
28
|
+
# which is the area code (NPA) on 11 digit phone numbers
|
29
|
+
#
|
30
|
+
# Usage:
|
31
|
+
# find('15183332220', 'Default') # Specify context 'Default'
|
32
|
+
# find('15183332220') # Set context to '518' automatically
|
33
|
+
#
|
34
|
+
def find(mailbox, context = nil)
|
35
|
+
if context
|
36
|
+
self.current_mailbox = new(mailbox, context)
|
37
|
+
else
|
38
|
+
self.current_mailbox = new(mailbox, mailbox.to_s[1, 3])
|
39
|
+
end
|
40
|
+
yield(current_mailbox) if block_given?
|
41
|
+
current_mailbox
|
42
|
+
end
|
43
|
+
alias :[] :find
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :mailbox, :context
|
47
|
+
|
48
|
+
#
|
49
|
+
# Create a new Mailbox object
|
50
|
+
#
|
51
|
+
def initialize(mailbox, context)
|
52
|
+
@context = context.to_s
|
53
|
+
@mailbox = mailbox.to_s
|
54
|
+
|
55
|
+
unless File.exists?(mailbox_path)
|
56
|
+
raise ActiveMailbox::Errors::MailboxNotFound, "`#{mailbox_path}` does not exist"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Compare based on path name
|
62
|
+
#
|
63
|
+
def <=>(other)
|
64
|
+
mailbox <=> other.mailbox
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Use method missing to simulate accessors for folders
|
69
|
+
#
|
70
|
+
# Eg: self.inbox is the same as self.folders[:inbox]
|
71
|
+
#
|
72
|
+
def method_missing(method_name, *args, &block)
|
73
|
+
meth = method_name.to_s.downcase.gsub(/\W/, '_').to_sym
|
74
|
+
if folders.has_key?(meth)
|
75
|
+
folders[meth]
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Returns true if method_name is a folder name, or calls super
|
83
|
+
#
|
84
|
+
def respond_to?(method_name)
|
85
|
+
folders.has_key?(method_name.to_s.downcase.gsub(/\W/, '_').to_sym) || super
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# The full filepath to this Mailbox
|
90
|
+
#
|
91
|
+
def path
|
92
|
+
@path ||= mailbox_path
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# A hash/struct of folders in this Mailbox.
|
97
|
+
# Keys are the folder name (as a symbol).
|
98
|
+
# Values are an array of Message objects
|
99
|
+
#
|
100
|
+
def folders(reload = false)
|
101
|
+
if reload or @reload_folders or ! defined?(@folders)
|
102
|
+
@folders = {}
|
103
|
+
Dir.chdir(mailbox_path) do
|
104
|
+
Dir['*'].each do |folder|
|
105
|
+
if File.directory?(folder) && ! ignore_dirs.include?(folder)
|
106
|
+
key = folder.downcase.gsub(/\W/, '_').to_sym
|
107
|
+
@folders[key] = Folder.new("#{Dir.pwd}/#{folder}", self)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
@folders
|
113
|
+
ensure
|
114
|
+
@reload_folders = false
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Return total number of messages in every folder
|
119
|
+
#
|
120
|
+
def total_messages
|
121
|
+
folders.values.inject(0) { |sum, folder| sum += folder.size }
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Destroy all Messages in this mailbox, but leave Mailbox,
|
126
|
+
# Folders, and greetings intact
|
127
|
+
#
|
128
|
+
def purge!
|
129
|
+
folders.each do |name, folder|
|
130
|
+
folder.purge!
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Destroy this Mailbox and all messages/greetings
|
136
|
+
#
|
137
|
+
def destroy!
|
138
|
+
FileUtils.rm_rf(mailbox_path)
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Sort all Messages in all Folders
|
143
|
+
#
|
144
|
+
# See ActiveMailbox::Folder#sort! for more info
|
145
|
+
#
|
146
|
+
def sort!
|
147
|
+
folders.each do |name, folder|
|
148
|
+
folder.sort!
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
#
|
153
|
+
# The greeting Asterisk will play
|
154
|
+
#
|
155
|
+
def current_greeting
|
156
|
+
case
|
157
|
+
when greeting_exists?(:temp)
|
158
|
+
@current_greeting = greeting_path(:temp)
|
159
|
+
when greeting_exists?(:unavail)
|
160
|
+
@current_greeting = greeting_path(:unavail)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# Delete greeting
|
166
|
+
#
|
167
|
+
# Valid options: :unavail, :temp, :busy
|
168
|
+
#
|
169
|
+
def delete_greeting!(greeting = :unavail)
|
170
|
+
if ValidGreetings.include?(greeting)
|
171
|
+
greeting_exists?(greeting) && File.unlink(greeting_path(greeting))
|
172
|
+
else
|
173
|
+
raise ActiveMailbox::Errors::GreetingNotFound, "Invalid greeting `#{greeting}'"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Delete temp.wav
|
179
|
+
#
|
180
|
+
def delete_temp_greeting!
|
181
|
+
greeting_exists?(:temp) && File.unlink(greeting_path(:temp))
|
182
|
+
end
|
183
|
+
|
184
|
+
#
|
185
|
+
# Delete busy.wav
|
186
|
+
#
|
187
|
+
def delete_busy_greeting!
|
188
|
+
greeting_exists?(:busy) && File.unlink(greeting_path(:busy))
|
189
|
+
end
|
190
|
+
|
191
|
+
#
|
192
|
+
# Delete unavail.wav
|
193
|
+
#
|
194
|
+
def delete_unavail_greeting!
|
195
|
+
greeting_exists?(:unavail) && File.unlink(greeting_path(:unavail))
|
196
|
+
end
|
197
|
+
|
198
|
+
#
|
199
|
+
# Deletes 'ghost' Messages from all Folders
|
200
|
+
#
|
201
|
+
# See ActiveMailbox::Folder#clean_ghosts! for
|
202
|
+
# info on 'ghost' voicemails in Asterisk
|
203
|
+
#
|
204
|
+
def clean_ghosts!(autosort = true)
|
205
|
+
folders.each do |name, folder|
|
206
|
+
folder.clean_ghosts!(autosort)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
#
|
211
|
+
# Deletes Messages older than 30 days from all Folders
|
212
|
+
#
|
213
|
+
def clean_stale!(autosort = true)
|
214
|
+
folders.each do |name, folder|
|
215
|
+
folder.clean_stale!(autosort)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
#
|
220
|
+
# Path to greeting
|
221
|
+
#
|
222
|
+
def greeting_path(greeting = :unavail)
|
223
|
+
"#{mailbox_path}/#{greeting}.wav"
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
#
|
229
|
+
# Check if greeting file exists
|
230
|
+
#
|
231
|
+
def greeting_exists?(greeting = :unavail)
|
232
|
+
ValidGreetings.include?(greeting) && File.exist?(greeting_path(greeting))
|
233
|
+
end
|
234
|
+
|
235
|
+
#
|
236
|
+
# Path to mailbox
|
237
|
+
#
|
238
|
+
def mailbox_path
|
239
|
+
"%s/%s/%s" % [ActiveMailbox::VOICEMAIL_ROOT, @context, @mailbox]
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# These dirs are used by Asterisk and don't contain anything
|
244
|
+
# ActiveMailbox is interested in
|
245
|
+
#
|
246
|
+
def ignore_dirs
|
247
|
+
%w[tmp temp unavail]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module ActiveMailbox
|
4
|
+
#
|
5
|
+
# The ActiveMailbox::Message class represents an Asterisk voicemail
|
6
|
+
#
|
7
|
+
class Message
|
8
|
+
#
|
9
|
+
# Maximum age before a Message is considered stale (30 days)
|
10
|
+
#
|
11
|
+
MaximumAge = 2592000
|
12
|
+
|
13
|
+
include Comparable
|
14
|
+
|
15
|
+
#
|
16
|
+
# Values from Asterisk's message info file (eg: msg0001.txt)
|
17
|
+
# that are used by this class
|
18
|
+
#
|
19
|
+
InfoFileKeys = %w[callerid origdate duration]
|
20
|
+
|
21
|
+
attr_reader :folder
|
22
|
+
|
23
|
+
def initialize(info_file, folder)
|
24
|
+
unless File.exists?(info_file)
|
25
|
+
raise ActiveMailbox::Errors::MessageNotFound, "#{info_file} does not exist"
|
26
|
+
end
|
27
|
+
@folder = folder
|
28
|
+
@info_file = info_file
|
29
|
+
parse_data!
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# The name of this message (ex: msg0001)
|
34
|
+
#
|
35
|
+
def name
|
36
|
+
@name ||= path.split('/').last
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# The number pulled from name
|
41
|
+
#
|
42
|
+
def number
|
43
|
+
@number ||= (num = name.match(/([0-9]{4})$/) and num[1].to_i)
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# Compare messages by the number in the name
|
48
|
+
#
|
49
|
+
def <=>(other)
|
50
|
+
number <=> other.number
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Destroy this Message's wav and txt files
|
55
|
+
#
|
56
|
+
def destroy!
|
57
|
+
[txt, wav].each do |file|
|
58
|
+
File.unlink(file)
|
59
|
+
end
|
60
|
+
ensure
|
61
|
+
@folder.reload_messages = true
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# The time the Message was left
|
66
|
+
#
|
67
|
+
def timestamp
|
68
|
+
@timestamp ||= Time.parse(@origdate)
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# The caller's phone number (from CallerID)
|
73
|
+
#
|
74
|
+
def callerid_number
|
75
|
+
@callerid_number ||= @callerid.gsub(/.*\<(.*)\>/, '\1')
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# The caller's name (from CallerID)
|
80
|
+
#
|
81
|
+
def callerid_name
|
82
|
+
@callerid_name ||= @callerid.gsub(/(.*)\s*\<.*\>/, '\1').strip
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# The duration of this Message (in seconds)
|
87
|
+
#
|
88
|
+
def duration
|
89
|
+
@duration
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# The file path to this Message's wav file
|
94
|
+
#
|
95
|
+
def wav
|
96
|
+
@wav ||= @info_file.sub(/txt$/, 'wav')
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# The file path to this Message's txt file
|
101
|
+
#
|
102
|
+
def txt
|
103
|
+
@info_file
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Returns msgXXXX
|
108
|
+
#
|
109
|
+
def path
|
110
|
+
@msg ||= txt.sub('.txt', '')
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Checks if this message is stale
|
115
|
+
#
|
116
|
+
def stale?
|
117
|
+
@stale ||= Time.now.to_i - timestamp.to_i > MaximumAge
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def data #:nodoc:
|
123
|
+
@data ||= File.read(@info_file)
|
124
|
+
end
|
125
|
+
|
126
|
+
def parse_data! #:nodoc:
|
127
|
+
data.lines.each do |line|
|
128
|
+
key, value = line.split('=')
|
129
|
+
if InfoFileKeys.include?(key)
|
130
|
+
instance_variable_set("@#{key}", value.chomp)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
data/test/folder_test.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class FolderTest < Test::Unit::TestCase
|
4
|
+
context "ActiveMailbox::Folder" do
|
5
|
+
setup do
|
6
|
+
ActiveMailbox::Fixtures.create!
|
7
|
+
@mailbox = ActiveMailbox::Mailbox.find('15183332220')
|
8
|
+
@folder = @mailbox.inbox
|
9
|
+
end
|
10
|
+
|
11
|
+
teardown do
|
12
|
+
ActiveMailbox::Fixtures.destroy!
|
13
|
+
end
|
14
|
+
|
15
|
+
should "raise FolderNotFound with an invalid path" do
|
16
|
+
assert_raise ActiveMailbox::Errors::FolderNotFound do
|
17
|
+
ActiveMailbox::Folder.new('i am not real!', @mailbox)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "instance" do
|
22
|
+
should "yield an array of Message objects" do
|
23
|
+
assert @folder.messages.is_a?(Array)
|
24
|
+
assert @folder.messages.first.is_a?(ActiveMailbox::Message)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "destroy itself" do
|
28
|
+
path = @folder.path
|
29
|
+
@folder.destroy!
|
30
|
+
assert File.exists?(path) == false
|
31
|
+
end
|
32
|
+
|
33
|
+
should "sort and rename messages by filename" do
|
34
|
+
messages = @folder.messages.map(&:name)
|
35
|
+
assert messages.size == @folder.size
|
36
|
+
|
37
|
+
ActiveMailbox::Fixtures.simulate_unordered(@folder)
|
38
|
+
assert messages.size > @folder.size
|
39
|
+
|
40
|
+
names = messages.map { |m| "msg%04d" % m.match(/([0-9]{4})/)[1].to_i }
|
41
|
+
check = (0..messages.size - 1).map { |i| "msg%04d" % i }
|
42
|
+
assert_same_elements names, check
|
43
|
+
end
|
44
|
+
|
45
|
+
should "clean ghost messages" do
|
46
|
+
ActiveMailbox::Fixtures.simulate_ghosts(@folder)
|
47
|
+
count = @folder.size
|
48
|
+
@folder.clean_ghosts!
|
49
|
+
assert count > @folder.size
|
50
|
+
end
|
51
|
+
|
52
|
+
should "purge all messages" do
|
53
|
+
@folder.purge!
|
54
|
+
assert @folder.size == 0
|
55
|
+
end
|
56
|
+
|
57
|
+
should "clean stale messages" do
|
58
|
+
count = @folder.size
|
59
|
+
@folder.clean_stale!
|
60
|
+
assert @folder.size < count
|
61
|
+
end
|
62
|
+
|
63
|
+
should "be compared to another instance by path" do
|
64
|
+
mailbox = ActiveMailbox::Mailbox.find(@mailbox.mailbox)
|
65
|
+
folder = ActiveMailbox::Folder.new(@folder.path, mailbox)
|
66
|
+
assert folder == @folder
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|