active_mailbox 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|