##// END OF EJS Templates
Merged rails-3.2 branch....
Jean-Philippe Lang -
r9346:5e57a1a9d947
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,4
1 # This file is used by Rack-based servers to start the application.
2
3 require ::File.expand_path('../config/environment', __FILE__)
4 run RedmineApp::Application
@@ -0,0 +1,56
1 require File.expand_path('../boot', __FILE__)
2
3 require 'rails/all'
4
5 if defined?(Bundler)
6 # If you precompile assets before deploying to production, use this line
7 Bundler.require(*Rails.groups(:assets => %w(development test)))
8 # If you want your assets lazily compiled in production, use this line
9 # Bundler.require(:default, :assets, Rails.env)
10 end
11
12 module RedmineApp
13 class Application < Rails::Application
14 # Settings in config/environments/* take precedence over those specified here.
15 # Application configuration should go into files in config/initializers
16 # -- all .rb files in that directory are automatically loaded.
17
18 # Custom directories with classes and modules you want to be autoloadable.
19 config.autoload_paths += %W(#{config.root}/lib)
20
21 # Only load the plugins named here, in the order given (default is alphabetical).
22 # :all can be used as a placeholder for all plugins not explicitly named.
23 # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24
25 # Activate observers that should always be running.
26 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
27
28 config.active_record.store_full_sti_class = true
29
30 # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
31 # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
32 # config.time_zone = 'Central Time (US & Canada)'
33
34 # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
35 # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
36 # config.i18n.default_locale = :de
37
38 # Configure the default encoding used in templates for Ruby 1.9.
39 config.encoding = "utf-8"
40
41 # Configure sensitive parameters which will be filtered from the log file.
42 config.filter_parameters += [:password]
43
44 # Enable the asset pipeline
45 config.assets.enabled = false
46
47 # Version of your assets, change this if you want to expire all your assets
48 config.assets.version = '1.0'
49
50 config.action_mailer.perform_deliveries = false
51
52 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
53 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
54 end
55 end
56 end
@@ -0,0 +1,17
1 class ChangeRepositoriesToFullSti < ActiveRecord::Migration
2 def up
3 Repository.connection.select_rows("SELECT id, type FROM #{Repository.table_name}").each do |repository_id, repository_type|
4 unless repository_type =~ /^Repository::/
5 Repository.update_all ["type = ?", "Repository::#{repository_type}"], ["id = ?", repository_id]
6 end
7 end
8 end
9
10 def down
11 Repository.connection.select_rows("SELECT id, type FROM #{Repository.table_name}").each do |repository_id, repository_type|
12 if repository_type =~ /^Repository::(.+)$/
13 Repository.update_all ["type = ?", $1], ["id = ?", repository_id]
14 end
15 end
16 end
17 end
@@ -0,0 +1,7
1 # Plugin's routes
2 # See: http://guides.rubyonrails.org/routing.html
3
4 match 'projects/:id/hello', :to => 'example#say_hello', :via => 'get'
5 match 'projects/:id/bye', :to => 'example#say_goodbye', :via => 'get'
6
7 resources 'meetings'
@@ -0,0 +1,2
1 # Plugin's routes
2 # See: http://guides.rubyonrails.org/routing.html
@@ -0,0 +1,6
1 require 'tmail/version'
2 require 'tmail/mail'
3 require 'tmail/mailbox'
4 require 'tmail/core_extensions'
5 require 'tmail/net'
6 require 'tmail/vendor/rchardet-1.3/lib/rchardet' No newline at end of file
@@ -0,0 +1,18
1 # lib/tmail/Makefile
2 #
3
4 debug:
5 rm -f parser.rb
6 make parser.rb DEBUG=true
7
8 parser.rb: parser.y
9 if [ "$(DEBUG)" = true ]; then \
10 racc -v -g -o$@ parser.y ;\
11 else \
12 racc -E -o$@ parser.y ;\
13 fi
14
15 clean:
16 rm -f parser.rb parser.output
17
18 distclean: clean
@@ -0,0 +1,392
1 =begin rdoc
2
3 = Address handling class
4
5 =end
6 #--
7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
30 #++
31
32 require 'tmail/encode'
33 require 'tmail/parser'
34
35
36 module TMail
37
38 # = Class Address
39 #
40 # Provides a complete handling library for email addresses. Can parse a string of an
41 # address directly or take in preformatted addresses themselves. Allows you to add
42 # and remove phrases from the front of the address and provides a compare function for
43 # email addresses.
44 #
45 # == Parsing and Handling a Valid Address:
46 #
47 # Just pass the email address in as a string to Address.parse:
48 #
49 # email = TMail::Address.parse('Mikel Lindsaar <mikel@lindsaar.net>')
50 # #=> #<TMail::Address mikel@lindsaar.net>
51 # email.address
52 # #=> "mikel@lindsaar.net"
53 # email.local
54 # #=> "mikel"
55 # email.domain
56 # #=> "lindsaar.net"
57 # email.name # Aliased as phrase as well
58 # #=> "Mikel Lindsaar"
59 #
60 # == Detecting an Invalid Address
61 #
62 # If you want to check the syntactical validity of an email address, just pass it to
63 # Address.parse and catch any SyntaxError:
64 #
65 # begin
66 # TMail::Address.parse("mikel 2@@@@@ me .com")
67 # rescue TMail::SyntaxError
68 # puts("Invalid Email Address Detected")
69 # else
70 # puts("Address is valid")
71 # end
72 # #=> "Invalid Email Address Detected"
73 class Address
74
75 include TextUtils #:nodoc:
76
77 # Sometimes you need to parse an address, TMail can do it for you and provide you with
78 # a fairly robust method of detecting a valid address.
79 #
80 # Takes in a string, returns a TMail::Address object.
81 #
82 # Raises a TMail::SyntaxError on invalid email format
83 def Address.parse( str )
84 Parser.parse :ADDRESS, str
85 end
86
87 def address_group? #:nodoc:
88 false
89 end
90
91 # Address.new(local, domain)
92 #
93 # Accepts:
94 #
95 # * local - Left of the at symbol
96 #
97 # * domain - Array of the domain split at the periods.
98 #
99 # For example:
100 #
101 # Address.new("mikel", ["lindsaar", "net"])
102 # #=> "#<TMail::Address mikel@lindsaar.net>"
103 def initialize( local, domain )
104 if domain
105 domain.each do |s|
106 raise SyntaxError, 'empty word in domain' if s.empty?
107 end
108 end
109
110 # This is to catch an unquoted "@" symbol in the local part of the
111 # address. Handles addresses like <"@"@me.com> and makes sure they
112 # stay like <"@"@me.com> (previously were becoming <@@me.com>)
113 if local && (local.join == '@' || local.join =~ /\A[^"].*?@.*?[^"]\Z/)
114 @local = "\"#{local.join}\""
115 else
116 @local = local
117 end
118
119 @domain = domain
120 @name = nil
121 @routes = []
122 end
123
124 # Provides the name or 'phrase' of the email address.
125 #
126 # For Example:
127 #
128 # email = TMail::Address.parse("Mikel Lindsaar <mikel@lindsaar.net>")
129 # email.name
130 # #=> "Mikel Lindsaar"
131 def name
132 @name
133 end
134
135 # Setter method for the name or phrase of the email
136 #
137 # For Example:
138 #
139 # email = TMail::Address.parse("mikel@lindsaar.net")
140 # email.name
141 # #=> nil
142 # email.name = "Mikel Lindsaar"
143 # email.to_s
144 # #=> "Mikel Lindsaar <mikel@me.com>"
145 def name=( str )
146 @name = str
147 @name = nil if str and str.empty?
148 end
149
150 #:stopdoc:
151 alias phrase name
152 alias phrase= name=
153 #:startdoc:
154
155 # This is still here from RFC 822, and is now obsolete per RFC2822 Section 4.
156 #
157 # "When interpreting addresses, the route portion SHOULD be ignored."
158 #
159 # It is still here, so you can access it.
160 #
161 # Routes return the route portion at the front of the email address, if any.
162 #
163 # For Example:
164 # email = TMail::Address.parse( "<@sa,@another:Mikel@me.com>")
165 # => #<TMail::Address Mikel@me.com>
166 # email.to_s
167 # => "<@sa,@another:Mikel@me.com>"
168 # email.routes
169 # => ["sa", "another"]
170 def routes
171 @routes
172 end
173
174 def inspect #:nodoc:
175 "#<#{self.class} #{address()}>"
176 end
177
178 # Returns the local part of the email address
179 #
180 # For Example:
181 #
182 # email = TMail::Address.parse("mikel@lindsaar.net")
183 # email.local
184 # #=> "mikel"
185 def local
186 return nil unless @local
187 return '""' if @local.size == 1 and @local[0].empty?
188 # Check to see if it is an array before trying to map it
189 if @local.respond_to?(:map)
190 @local.map {|i| quote_atom(i) }.join('.')
191 else
192 quote_atom(@local)
193 end
194 end
195
196 # Returns the domain part of the email address
197 #
198 # For Example:
199 #
200 # email = TMail::Address.parse("mikel@lindsaar.net")
201 # email.local
202 # #=> "lindsaar.net"
203 def domain
204 return nil unless @domain
205 join_domain(@domain)
206 end
207
208 # Returns the full specific address itself
209 #
210 # For Example:
211 #
212 # email = TMail::Address.parse("mikel@lindsaar.net")
213 # email.address
214 # #=> "mikel@lindsaar.net"
215 def spec
216 s = self.local
217 d = self.domain
218 if s and d
219 s + '@' + d
220 else
221 s
222 end
223 end
224
225 alias address spec
226
227 # Provides == function to the email. Only checks the actual address
228 # and ignores the name/phrase component
229 #
230 # For Example
231 #
232 # addr1 = TMail::Address.parse("My Address <mikel@lindsaar.net>")
233 # #=> "#<TMail::Address mikel@lindsaar.net>"
234 # addr2 = TMail::Address.parse("Another <mikel@lindsaar.net>")
235 # #=> "#<TMail::Address mikel@lindsaar.net>"
236 # addr1 == addr2
237 # #=> true
238 def ==( other )
239 other.respond_to? :spec and self.spec == other.spec
240 end
241
242 alias eql? ==
243
244 # Provides a unique hash value for this record against the local and domain
245 # parts, ignores the name/phrase value
246 #
247 # email = TMail::Address.parse("mikel@lindsaar.net")
248 # email.hash
249 # #=> 18767598
250 def hash
251 @local.hash ^ @domain.hash
252 end
253
254 # Duplicates a TMail::Address object returning the duplicate
255 #
256 # addr1 = TMail::Address.parse("mikel@lindsaar.net")
257 # addr2 = addr1.dup
258 # addr1.id == addr2.id
259 # #=> false
260 def dup
261 obj = self.class.new(@local.dup, @domain.dup)
262 obj.name = @name.dup if @name
263 obj.routes.replace @routes
264 obj
265 end
266
267 include StrategyInterface #:nodoc:
268
269 def accept( strategy, dummy1 = nil, dummy2 = nil ) #:nodoc:
270 unless @local
271 strategy.meta '<>' # empty return-path
272 return
273 end
274
275 spec_p = (not @name and @routes.empty?)
276 if @name
277 strategy.phrase @name
278 strategy.space
279 end
280 tmp = spec_p ? '' : '<'
281 unless @routes.empty?
282 tmp << @routes.map {|i| '@' + i }.join(',') << ':'
283 end
284 tmp << self.spec
285 tmp << '>' unless spec_p
286 strategy.meta tmp
287 strategy.lwsp ''
288 end
289
290 end
291
292
293 class AddressGroup
294
295 include Enumerable
296
297 def address_group?
298 true
299 end
300
301 def initialize( name, addrs )
302 @name = name
303 @addresses = addrs
304 end
305
306 attr_reader :name
307
308 def ==( other )
309 other.respond_to? :to_a and @addresses == other.to_a
310 end
311
312 alias eql? ==
313
314 def hash
315 map {|i| i.hash }.hash
316 end
317
318 def []( idx )
319 @addresses[idx]
320 end
321
322 def size
323 @addresses.size
324 end
325
326 def empty?
327 @addresses.empty?
328 end
329
330 def each( &block )
331 @addresses.each(&block)
332 end
333
334 def to_a
335 @addresses.dup
336 end
337
338 alias to_ary to_a
339
340 def include?( a )
341 @addresses.include? a
342 end
343
344 def flatten
345 set = []
346 @addresses.each do |a|
347 if a.respond_to? :flatten
348 set.concat a.flatten
349 else
350 set.push a
351 end
352 end
353 set
354 end
355
356 def each_address( &block )
357 flatten.each(&block)
358 end
359
360 def add( a )
361 @addresses.push a
362 end
363
364 alias push add
365
366 def delete( a )
367 @addresses.delete a
368 end
369
370 include StrategyInterface
371
372 def accept( strategy, dummy1 = nil, dummy2 = nil )
373 strategy.phrase @name
374 strategy.meta ':'
375 strategy.space
376 first = true
377 each do |mbox|
378 if first
379 first = false
380 else
381 strategy.puts_meta ','
382 end
383 strategy.space
384 mbox.accept strategy
385 end
386 strategy.meta ';'
387 strategy.lwsp ''
388 end
389
390 end
391
392 end # module TMail
@@ -0,0 +1,65
1 =begin rdoc
2
3 = Attachment handling file
4
5 =end
6
7 require 'kconv'
8 require 'stringio'
9
10 module TMail
11 class Attachment < StringIO
12 attr_accessor :original_filename, :content_type
13 alias quoted_filename original_filename
14 end
15
16 class Mail
17 def has_attachments?
18 attachment?(self) || multipart? && parts.any? { |part| attachment?(part) }
19 end
20
21 # Returns true if this part's content main type is text, else returns false.
22 # By main type is meant "text/plain" is text. "text/html" is text
23 def text_content_type?
24 self.header['content-type'] && (self.header['content-type'].main_type == 'text')
25 end
26
27 def inline_attachment?(part)
28 part['content-id'] || (part['content-disposition'] && part['content-disposition'].disposition == 'inline' && !part.text_content_type?)
29 end
30
31 def attachment?(part)
32 part.disposition_is_attachment? || (!part.content_type.nil? && !part.text_content_type?) unless part.multipart?
33 end
34
35 def attachments
36 if multipart?
37 parts.collect { |part| attachment(part) }.flatten.compact
38 elsif attachment?(self)
39 [attachment(self)]
40 end
41 end
42
43 private
44
45 def attachment(part)
46 if part.multipart?
47 part.attachments
48 elsif attachment?(part)
49 content = part.body # unquoted automatically by TMail#body
50 file_name = (part['content-location'] && part['content-location'].body) ||
51 part.sub_header('content-type', 'name') ||
52 part.sub_header('content-disposition', 'filename') ||
53 'noname'
54
55 return if content.blank?
56
57 attachment = TMail::Attachment.new(content)
58 attachment.original_filename = file_name.strip unless file_name.blank?
59 attachment.content_type = part.content_type
60 attachment
61 end
62 end
63
64 end
65 end
@@ -0,0 +1,46
1 #--
2 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 #
23 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24 # with permission of Minero Aoki.
25 #++
26 #:stopdoc:
27 module TMail
28 module Base64
29
30 module_function
31
32 def folding_encode( str, eol = "\n", limit = 60 )
33 [str].pack('m')
34 end
35
36 def encode( str )
37 [str].pack('m').tr( "\r\n", '' )
38 end
39
40 def decode( str, strict = false )
41 str.unpack('m').first
42 end
43
44 end
45 end
46 #:startdoc:
@@ -0,0 +1,41
1 #:stopdoc:
2 unless Enumerable.method_defined?(:map)
3 module Enumerable #:nodoc:
4 alias map collect
5 end
6 end
7
8 unless Enumerable.method_defined?(:select)
9 module Enumerable #:nodoc:
10 alias select find_all
11 end
12 end
13
14 unless Enumerable.method_defined?(:reject)
15 module Enumerable #:nodoc:
16 def reject
17 result = []
18 each do |i|
19 result.push i unless yield(i)
20 end
21 result
22 end
23 end
24 end
25
26 unless Enumerable.method_defined?(:sort_by)
27 module Enumerable #:nodoc:
28 def sort_by
29 map {|i| [yield(i), i] }.sort.map {|val, i| i }
30 end
31 end
32 end
33
34 unless File.respond_to?(:read)
35 def File.read(fname) #:nodoc:
36 File.open(fname) {|f|
37 return f.read
38 }
39 end
40 end
41 #:startdoc: No newline at end of file
@@ -0,0 +1,67
1 #--
2 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 #
23 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24 # with permission of Minero Aoki.
25 #++
26 #:stopdoc:
27 module TMail
28
29 class Config
30
31 def initialize( strict )
32 @strict_parse = strict
33 @strict_base64decode = strict
34 end
35
36 def strict_parse?
37 @strict_parse
38 end
39
40 attr_writer :strict_parse
41
42 def strict_base64decode?
43 @strict_base64decode
44 end
45
46 attr_writer :strict_base64decode
47
48 def new_body_port( mail )
49 StringPort.new
50 end
51
52 alias new_preamble_port new_body_port
53 alias new_part_port new_body_port
54
55 end
56
57 DEFAULT_CONFIG = Config.new(false)
58 DEFAULT_STRICT_CONFIG = Config.new(true)
59
60 def Config.to_config( arg )
61 return DEFAULT_STRICT_CONFIG if arg == true
62 return DEFAULT_CONFIG if arg == false
63 arg or DEFAULT_CONFIG
64 end
65
66 end
67 #:startdoc: No newline at end of file
@@ -0,0 +1,63
1 #:stopdoc:
2 unless Object.respond_to?(:blank?)
3 class Object
4 # Check first to see if we are in a Rails environment, no need to
5 # define these methods if we are
6
7 # An object is blank if it's nil, empty, or a whitespace string.
8 # For example, "", " ", nil, [], and {} are blank.
9 #
10 # This simplifies
11 # if !address.nil? && !address.empty?
12 # to
13 # if !address.blank?
14 def blank?
15 if respond_to?(:empty?) && respond_to?(:strip)
16 empty? or strip.empty?
17 elsif respond_to?(:empty?)
18 empty?
19 else
20 !self
21 end
22 end
23 end
24
25 class NilClass
26 def blank?
27 true
28 end
29 end
30
31 class FalseClass
32 def blank?
33 true
34 end
35 end
36
37 class TrueClass
38 def blank?
39 false
40 end
41 end
42
43 class Array
44 alias_method :blank?, :empty?
45 end
46
47 class Hash
48 alias_method :blank?, :empty?
49 end
50
51 class String
52 def blank?
53 empty? || strip.empty?
54 end
55 end
56
57 class Numeric
58 def blank?
59 false
60 end
61 end
62 end
63 #:startdoc: No newline at end of file
This diff has been collapsed as it changes many lines, (590 lines changed) Show them Hide them
@@ -0,0 +1,590
1 #--
2 # = COPYRIGHT:
3 #
4 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
5 #
6 # Permission is hereby granted, free of charge, to any person obtaining
7 # a copy of this software and associated documentation files (the
8 # "Software"), to deal in the Software without restriction, including
9 # without limitation the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the Software, and to
11 # permit persons to whom the Software is furnished to do so, subject to
12 # the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be
15 # included in all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 #
25 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
26 # with permission of Minero Aoki.
27 #++
28 #:stopdoc:
29 require 'nkf'
30 require 'tmail/base64'
31 require 'tmail/stringio'
32 require 'tmail/utils'
33 #:startdoc:
34
35
36 module TMail
37
38 #:stopdoc:
39 class << self
40 attr_accessor :KCODE
41 end
42 self.KCODE = 'NONE'
43
44 module StrategyInterface
45
46 def create_dest( obj )
47 case obj
48 when nil
49 StringOutput.new
50 when String
51 StringOutput.new(obj)
52 when IO, StringOutput
53 obj
54 else
55 raise TypeError, 'cannot handle this type of object for dest'
56 end
57 end
58 module_function :create_dest
59
60 #:startdoc:
61 # Returns the TMail object encoded and ready to be sent via SMTP etc.
62 # You should call this before you are packaging up your email to
63 # correctly escape all the values that need escaping in the email, line
64 # wrap the email etc.
65 #
66 # It is also a good idea to call this before you marshal or serialize
67 # a TMail object.
68 #
69 # For Example:
70 #
71 # email = TMail::Load(my_email_file)
72 # email_to_send = email.encoded
73 def encoded( eol = "\r\n", charset = 'j', dest = nil )
74 accept_strategy Encoder, eol, charset, dest
75 end
76
77 # Returns the TMail object decoded and ready to be used by you, your
78 # program etc.
79 #
80 # You should call this before you are packaging up your email to
81 # correctly escape all the values that need escaping in the email, line
82 # wrap the email etc.
83 #
84 # For Example:
85 #
86 # email = TMail::Load(my_email_file)
87 # email_to_send = email.encoded
88 def decoded( eol = "\n", charset = 'e', dest = nil )
89 # Turn the E-Mail into a string and return it with all
90 # encoded characters decoded. alias for to_s
91 accept_strategy Decoder, eol, charset, dest
92 end
93
94 alias to_s decoded
95
96 def accept_strategy( klass, eol, charset, dest = nil ) #:nodoc:
97 dest ||= ''
98 accept klass.new( create_dest(dest), charset, eol )
99 dest
100 end
101
102 end
103
104 #:stopdoc:
105
106 ###
107 ### MIME B encoding decoder
108 ###
109
110 class Decoder
111
112 include TextUtils
113
114 encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
115 ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
116 SPACER = "\t"
117
118 OUTPUT_ENCODING = {
119 'EUC' => 'e',
120 'SJIS' => 's',
121 }
122
123 def self.decode( str, encoding = nil )
124 encoding ||= (OUTPUT_ENCODING[TMail.KCODE] || 'j')
125 opt = '-mS' + encoding
126 str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
127 end
128
129 def initialize( dest, encoding = nil, eol = "\n" )
130 @f = StrategyInterface.create_dest(dest)
131 @encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
132 @eol = eol
133 end
134
135 def decode( str )
136 self.class.decode(str, @encoding)
137 end
138 private :decode
139
140 def terminate
141 end
142
143 def header_line( str )
144 @f << decode(str)
145 end
146
147 def header_name( nm )
148 @f << nm << ': '
149 end
150
151 def header_body( str )
152 @f << decode(str)
153 end
154
155 def space
156 @f << ' '
157 end
158
159 alias spc space
160
161 def lwsp( str )
162 @f << str
163 end
164
165 def meta( str )
166 @f << str
167 end
168
169 def puts_meta( str )
170 @f << str
171 end
172
173 def text( str )
174 @f << decode(str)
175 end
176
177 def phrase( str )
178 @f << quote_phrase(decode(str))
179 end
180
181 def kv_pair( k, v )
182 v = dquote(v) unless token_safe?(v)
183 @f << k << '=' << v
184 end
185
186 def puts( str = nil )
187 @f << str if str
188 @f << @eol
189 end
190
191 def write( str )
192 @f << str
193 end
194
195 end
196
197
198 ###
199 ### MIME B-encoding encoder
200 ###
201
202 #
203 # FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
204 #
205 class Encoder
206
207 include TextUtils
208
209 BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
210
211 def Encoder.encode( str )
212 e = new()
213 e.header_body str
214 e.terminate
215 e.dest.string
216 end
217
218 SPACER = "\t"
219 MAX_LINE_LEN = 78
220 RFC_2822_MAX_LENGTH = 998
221
222 OPTIONS = {
223 'EUC' => '-Ej -m0',
224 'SJIS' => '-Sj -m0',
225 'UTF8' => nil, # FIXME
226 'NONE' => nil
227 }
228
229 def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
230 @f = StrategyInterface.create_dest(dest)
231 @opt = OPTIONS[TMail.KCODE]
232 @eol = eol
233 @folded = false
234 @preserve_quotes = true
235 reset
236 end
237
238 def preserve_quotes=( bool )
239 @preserve_quotes
240 end
241
242 def preserve_quotes
243 @preserve_quotes
244 end
245
246 def normalize_encoding( str )
247 if @opt
248 then NKF.nkf(@opt, str)
249 else str
250 end
251 end
252
253 def reset
254 @text = ''
255 @lwsp = ''
256 @curlen = 0
257 end
258
259 def terminate
260 add_lwsp ''
261 reset
262 end
263
264 def dest
265 @f
266 end
267
268 def puts( str = nil )
269 @f << str if str
270 @f << @eol
271 end
272
273 def write( str )
274 @f << str
275 end
276
277 #
278 # add
279 #
280
281 def header_line( line )
282 scanadd line
283 end
284
285 def header_name( name )
286 add_text name.split(/-/).map {|i| i.capitalize }.join('-')
287 add_text ':'
288 add_lwsp ' '
289 end
290
291 def header_body( str )
292 scanadd normalize_encoding(str)
293 end
294
295 def space
296 add_lwsp ' '
297 end
298
299 alias spc space
300
301 def lwsp( str )
302 add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
303 end
304
305 def meta( str )
306 add_text str
307 end
308
309 def puts_meta( str )
310 add_text str + @eol + SPACER
311 end
312
313 def text( str )
314 scanadd normalize_encoding(str)
315 end
316
317 def phrase( str )
318 str = normalize_encoding(str)
319 if CONTROL_CHAR === str
320 scanadd str
321 else
322 add_text quote_phrase(str)
323 end
324 end
325
326 # FIXME: implement line folding
327 #
328 def kv_pair( k, v )
329 return if v.nil?
330 v = normalize_encoding(v)
331 if token_safe?(v)
332 add_text k + '=' + v
333 elsif not CONTROL_CHAR === v
334 add_text k + '=' + quote_token(v)
335 else
336 # apply RFC2231 encoding
337 kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
338 add_text kv
339 end
340 end
341
342 def encode_value( str )
343 str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
344 end
345
346 private
347
348 def scanadd( str, force = false )
349 types = ''
350 strs = []
351 if str.respond_to?(:encoding)
352 enc = str.encoding
353 str.force_encoding(Encoding::ASCII_8BIT)
354 end
355 until str.empty?
356 if m = /\A[^\e\t\r\n ]+/.match(str)
357 types << (force ? 'j' : 'a')
358 if str.respond_to?(:encoding)
359 strs.push m[0].force_encoding(enc)
360 else
361 strs.push m[0]
362 end
363 elsif m = /\A[\t\r\n ]+/.match(str)
364 types << 's'
365 if str.respond_to?(:encoding)
366 strs.push m[0].force_encoding(enc)
367 else
368 strs.push m[0]
369 end
370
371 elsif m = /\A\e../.match(str)
372 esc = m[0]
373 str = m.post_match
374 if esc != "\e(B" and m = /\A[^\e]+/.match(str)
375 types << 'j'
376 if str.respond_to?(:encoding)
377 strs.push m[0].force_encoding(enc)
378 else
379 strs.push m[0]
380 end
381 end
382
383 else
384 raise 'TMail FATAL: encoder scan fail'
385 end
386 (str = m.post_match) unless m.nil?
387 end
388
389 do_encode types, strs
390 end
391
392 def do_encode( types, strs )
393 #
394 # result : (A|E)(S(A|E))*
395 # E : W(SW)*
396 # W : (J|A)+ but must contain J # (J|A)*J(J|A)*
397 # A : <<A character string not to be encoded>>
398 # J : <<A character string to be encoded>>
399 # S : <<LWSP>>
400 #
401 # An encoding unit is `E'.
402 # Input (parameter `types') is (J|A)(J|A|S)*(J|A)
403 #
404 if BENCODE_DEBUG
405 puts
406 puts '-- do_encode ------------'
407 puts types.split(//).join(' ')
408 p strs
409 end
410
411 e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
412
413 while m = e.match(types)
414 pre = m.pre_match
415 concat_A_S pre, strs[0, pre.size] unless pre.empty?
416 concat_E m[0], strs[m.begin(0) ... m.end(0)]
417 types = m.post_match
418 strs.slice! 0, m.end(0)
419 end
420 concat_A_S types, strs
421 end
422
423 def concat_A_S( types, strs )
424 if RUBY_VERSION < '1.9'
425 a = ?a; s = ?s
426 else
427 a = 'a'.ord; s = 's'.ord
428 end
429 i = 0
430 types.each_byte do |t|
431 case t
432 when a then add_text strs[i]
433 when s then add_lwsp strs[i]
434 else
435 raise "TMail FATAL: unknown flag: #{t.chr}"
436 end
437 i += 1
438 end
439 end
440
441 METHOD_ID = {
442 ?j => :extract_J,
443 ?e => :extract_E,
444 ?a => :extract_A,
445 ?s => :extract_S
446 }
447
448 def concat_E( types, strs )
449 if BENCODE_DEBUG
450 puts '---- concat_E'
451 puts "types=#{types.split(//).join(' ')}"
452 puts "strs =#{strs.inspect}"
453 end
454
455 flush() unless @text.empty?
456
457 chunk = ''
458 strs.each_with_index do |s,i|
459 mid = METHOD_ID[types[i]]
460 until s.empty?
461 unless c = __send__(mid, chunk.size, s)
462 add_with_encode chunk unless chunk.empty?
463 flush
464 chunk = ''
465 fold
466 c = __send__(mid, 0, s)
467 raise 'TMail FATAL: extract fail' unless c
468 end
469 chunk << c
470 end
471 end
472 add_with_encode chunk unless chunk.empty?
473 end
474
475 def extract_J( chunksize, str )
476 size = max_bytes(chunksize, str.size) - 6
477 size = (size % 2 == 0) ? (size) : (size - 1)
478 return nil if size <= 0
479 if str.respond_to?(:encoding)
480 enc = str.encoding
481 str.force_encoding(Encoding::ASCII_8BIT)
482 "\e$B#{str.slice!(0, size)}\e(B".force_encoding(enc)
483 else
484 "\e$B#{str.slice!(0, size)}\e(B"
485 end
486 end
487
488 def extract_A( chunksize, str )
489 size = max_bytes(chunksize, str.size)
490 return nil if size <= 0
491 str.slice!(0, size)
492 end
493
494 alias extract_S extract_A
495
496 def max_bytes( chunksize, ssize )
497 (restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
498 end
499
500 #
501 # free length buffer
502 #
503
504 def add_text( str )
505 @text << str
506 # puts '---- text -------------------------------------'
507 # puts "+ #{str.inspect}"
508 # puts "txt >>>#{@text.inspect}<<<"
509 end
510
511 def add_with_encode( str )
512 @text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
513 end
514
515 def add_lwsp( lwsp )
516 # puts '---- lwsp -------------------------------------'
517 # puts "+ #{lwsp.inspect}"
518 fold if restsize() <= 0
519 flush(@folded)
520 @lwsp = lwsp
521 end
522
523 def flush(folded = false)
524 # puts '---- flush ----'
525 # puts "spc >>>#{@lwsp.inspect}<<<"
526 # puts "txt >>>#{@text.inspect}<<<"
527 @f << @lwsp << @text
528 if folded
529 @curlen = 0
530 else
531 @curlen += (@lwsp.size + @text.size)
532 end
533 @text = ''
534 @lwsp = ''
535 end
536
537 def fold
538 # puts '---- fold ----'
539 unless @f.string =~ /^.*?:$/
540 @f << @eol
541 @lwsp = SPACER
542 else
543 fold_header
544 @folded = true
545 end
546 @curlen = 0
547 end
548
549 def fold_header
550 # Called because line is too long - so we need to wrap.
551 # First look for whitespace in the text
552 # if it has text, fold there
553 # check the remaining text, if too long, fold again
554 # if it doesn't, then don't fold unless the line goes beyond 998 chars
555
556 # Check the text to see if there is whitespace, or if not
557 @wrapped_text = []
558 until @text.blank?
559 fold_the_string
560 end
561 @text = @wrapped_text.join("#{@eol}#{SPACER}")
562 end
563
564 def fold_the_string
565 whitespace_location = @text =~ /\s/ || @text.length
566 # Is the location of the whitespace shorter than the RCF_2822_MAX_LENGTH?
567 # if there is no whitespace in the string, then this
568 unless mazsize(whitespace_location) <= 0
569 @text.strip!
570 @wrapped_text << @text.slice!(0...whitespace_location)
571 # If it is not less, we have to wrap it destructively
572 else
573 slice_point = RFC_2822_MAX_LENGTH - @curlen - @lwsp.length
574 @text.strip!
575 @wrapped_text << @text.slice!(0...slice_point)
576 end
577 end
578
579 def restsize
580 MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
581 end
582
583 def mazsize(whitespace_location)
584 # Per RFC2822, the maximum length of a line is 998 chars
585 RFC_2822_MAX_LENGTH - (@curlen + @lwsp.size + whitespace_location)
586 end
587
588 end
589 #:startdoc:
590 end # module TMail
This diff has been collapsed as it changes many lines, (962 lines changed) Show them Hide them
@@ -0,0 +1,962
1 #--
2 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 #
23 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24 # with permission of Minero Aoki.
25 #++
26
27 require 'tmail/encode'
28 require 'tmail/address'
29 require 'tmail/parser'
30 require 'tmail/config'
31 require 'tmail/utils'
32
33 #:startdoc:
34 module TMail
35
36 # Provides methods to handle and manipulate headers in the email
37 class HeaderField
38
39 include TextUtils
40
41 class << self
42
43 alias newobj new
44
45 def new( name, body, conf = DEFAULT_CONFIG )
46 klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
47 klass.newobj body, conf
48 end
49
50 # Returns a HeaderField object matching the header you specify in the "name" param.
51 # Requires an initialized TMail::Port to be passed in.
52 #
53 # The method searches the header of the Port you pass into it to find a match on
54 # the header line you pass. Once a match is found, it will unwrap the matching line
55 # as needed to return an initialized HeaderField object.
56 #
57 # If you want to get the Envelope sender of the email object, pass in "EnvelopeSender",
58 # if you want the From address of the email itself, pass in 'From'.
59 #
60 # This is because a mailbox doesn't have the : after the From that designates the
61 # beginning of the envelope sender (which can be different to the from address of
62 # the email)
63 #
64 # Other fields can be passed as normal, "Reply-To", "Received" etc.
65 #
66 # Note: Change of behaviour in 1.2.1 => returns nil if it does not find the specified
67 # header field, otherwise returns an instantiated object of the correct header class
68 #
69 # For example:
70 # port = TMail::FilePort.new("/test/fixtures/raw_email_simple")
71 # h = TMail::HeaderField.new_from_port(port, "From")
72 # h.addrs.to_s #=> "Mikel Lindsaar <mikel@nowhere.com>"
73 # h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
74 # h.addrs.to_s #=> "mike@anotherplace.com.au"
75 # h = TMail::HeaderField.new_from_port(port, "SomeWeirdHeaderField")
76 # h #=> nil
77 def new_from_port( port, name, conf = DEFAULT_CONFIG )
78 if name == "EnvelopeSender"
79 name = "From"
80 re = Regexp.new('\A(From) ', 'i')
81 else
82 re = Regexp.new('\A(' + Regexp.quote(name) + '):', 'i')
83 end
84 str = nil
85 port.ropen {|f|
86 f.each do |line|
87 if m = re.match(line) then str = m.post_match.strip
88 elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
89 elsif /\A-*\s*\z/ === line then break
90 elsif str then break
91 end
92 end
93 }
94 new(name, str, Config.to_config(conf)) if str
95 end
96
97 def internal_new( name, conf )
98 FNAME_TO_CLASS[name].newobj('', conf, true)
99 end
100
101 end # class << self
102
103 def initialize( body, conf, intern = false )
104 @body = body
105 @config = conf
106
107 @illegal = false
108 @parsed = false
109
110 if intern
111 @parsed = true
112 parse_init
113 end
114 end
115
116 def inspect
117 "#<#{self.class} #{@body.inspect}>"
118 end
119
120 def illegal?
121 @illegal
122 end
123
124 def empty?
125 ensure_parsed
126 return true if @illegal
127 isempty?
128 end
129
130 private
131
132 def ensure_parsed
133 return if @parsed
134 @parsed = true
135 parse
136 end
137
138 # defabstract parse
139 # end
140
141 def clear_parse_status
142 @parsed = false
143 @illegal = false
144 end
145
146 public
147
148 def body
149 ensure_parsed
150 v = Decoder.new(s = '')
151 do_accept v
152 v.terminate
153 s
154 end
155
156 def body=( str )
157 @body = str
158 clear_parse_status
159 end
160
161 include StrategyInterface
162
163 def accept( strategy )
164 ensure_parsed
165 do_accept strategy
166 strategy.terminate
167 end
168
169 # abstract do_accept
170
171 end
172
173
174 class UnstructuredHeader < HeaderField
175
176 def body
177 ensure_parsed
178 @body
179 end
180
181 def body=( arg )
182 ensure_parsed
183 @body = arg
184 end
185
186 private
187
188 def parse_init
189 end
190
191 def parse
192 @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
193 end
194
195 def isempty?
196 not @body
197 end
198
199 def do_accept( strategy )
200 strategy.text @body
201 end
202
203 end
204
205
206 class StructuredHeader < HeaderField
207
208 def comments
209 ensure_parsed
210 if @comments[0]
211 [Decoder.decode(@comments[0])]
212 else
213 @comments
214 end
215 end
216
217 private
218
219 def parse
220 save = nil
221
222 begin
223 parse_init
224 do_parse
225 rescue SyntaxError
226 if not save and mime_encoded? @body
227 save = @body
228 @body = Decoder.decode(save)
229 retry
230 elsif save
231 @body = save
232 end
233
234 @illegal = true
235 raise if @config.strict_parse?
236 end
237 end
238
239 def parse_init
240 @comments = []
241 init
242 end
243
244 def do_parse
245 quote_boundary
246 quote_unquoted_name
247 quote_unquoted_bencode
248 obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
249 set obj if obj
250 end
251
252 end
253
254
255 class DateTimeHeader < StructuredHeader
256
257 PARSE_TYPE = :DATETIME
258
259 def date
260 ensure_parsed
261 @date
262 end
263
264 def date=( arg )
265 ensure_parsed
266 @date = arg
267 end
268
269 private
270
271 def init
272 @date = nil
273 end
274
275 def set( t )
276 @date = t
277 end
278
279 def isempty?
280 not @date
281 end
282
283 def do_accept( strategy )
284 strategy.meta time2str(@date)
285 end
286
287 end
288
289
290 class AddressHeader < StructuredHeader
291
292 PARSE_TYPE = :MADDRESS
293
294 def addrs
295 ensure_parsed
296 @addrs
297 end
298
299 private
300
301 def init
302 @addrs = []
303 end
304
305 def set( a )
306 @addrs = a
307 end
308
309 def isempty?
310 @addrs.empty?
311 end
312
313 def do_accept( strategy )
314 first = true
315 @addrs.each do |a|
316 if first
317 first = false
318 else
319 strategy.puts_meta ','
320 strategy.space
321 end
322 a.accept strategy
323 end
324
325 @comments.each do |c|
326 strategy.space
327 strategy.meta '('
328 strategy.text c
329 strategy.meta ')'
330 end
331 end
332
333 end
334
335
336 class ReturnPathHeader < AddressHeader
337
338 PARSE_TYPE = :RETPATH
339
340 def addr
341 addrs()[0]
342 end
343
344 def spec
345 a = addr() or return nil
346 a.spec
347 end
348
349 def routes
350 a = addr() or return nil
351 a.routes
352 end
353
354 private
355
356 def do_accept( strategy )
357 a = addr()
358
359 strategy.meta '<'
360 unless a.routes.empty?
361 strategy.meta a.routes.map {|i| '@' + i }.join(',')
362 strategy.meta ':'
363 end
364 spec = a.spec
365 strategy.meta spec if spec
366 strategy.meta '>'
367 end
368
369 end
370
371
372 class SingleAddressHeader < AddressHeader
373
374 def addr
375 addrs()[0]
376 end
377
378 private
379
380 def do_accept( strategy )
381 a = addr()
382 a.accept strategy
383 @comments.each do |c|
384 strategy.space
385 strategy.meta '('
386 strategy.text c
387 strategy.meta ')'
388 end
389 end
390
391 end
392
393
394 class MessageIdHeader < StructuredHeader
395
396 def id
397 ensure_parsed
398 @id
399 end
400
401 def id=( arg )
402 ensure_parsed
403 @id = arg
404 end
405
406 private
407
408 def init
409 @id = nil
410 end
411
412 def isempty?
413 not @id
414 end
415
416 def do_parse
417 @id = @body.slice(MESSAGE_ID) or
418 raise SyntaxError, "wrong Message-ID format: #{@body}"
419 end
420
421 def do_accept( strategy )
422 strategy.meta @id
423 end
424
425 end
426
427
428 class ReferencesHeader < StructuredHeader
429
430 def refs
431 ensure_parsed
432 @refs
433 end
434
435 def each_id
436 self.refs.each do |i|
437 yield i if MESSAGE_ID === i
438 end
439 end
440
441 def ids
442 ensure_parsed
443 @ids
444 end
445
446 def each_phrase
447 self.refs.each do |i|
448 yield i unless MESSAGE_ID === i
449 end
450 end
451
452 def phrases
453 ret = []
454 each_phrase {|i| ret.push i }
455 ret
456 end
457
458 private
459
460 def init
461 @refs = []
462 @ids = []
463 end
464
465 def isempty?
466 @ids.empty?
467 end
468
469 def do_parse
470 str = @body
471 while m = MESSAGE_ID.match(str)
472 pre = m.pre_match.strip
473 @refs.push pre unless pre.empty?
474 @refs.push s = m[0]
475 @ids.push s
476 str = m.post_match
477 end
478 str = str.strip
479 @refs.push str unless str.empty?
480 end
481
482 def do_accept( strategy )
483 first = true
484 @ids.each do |i|
485 if first
486 first = false
487 else
488 strategy.space
489 end
490 strategy.meta i
491 end
492 end
493
494 end
495
496
497 class ReceivedHeader < StructuredHeader
498
499 PARSE_TYPE = :RECEIVED
500
501 def from
502 ensure_parsed
503 @from
504 end
505
506 def from=( arg )
507 ensure_parsed
508 @from = arg
509 end
510
511 def by
512 ensure_parsed
513 @by
514 end
515
516 def by=( arg )
517 ensure_parsed
518 @by = arg
519 end
520
521 def via
522 ensure_parsed
523 @via
524 end
525
526 def via=( arg )
527 ensure_parsed
528 @via = arg
529 end
530
531 def with
532 ensure_parsed
533 @with
534 end
535
536 def id
537 ensure_parsed
538 @id
539 end
540
541 def id=( arg )
542 ensure_parsed
543 @id = arg
544 end
545
546 def _for
547 ensure_parsed
548 @_for
549 end
550
551 def _for=( arg )
552 ensure_parsed
553 @_for = arg
554 end
555
556 def date
557 ensure_parsed
558 @date
559 end
560
561 def date=( arg )
562 ensure_parsed
563 @date = arg
564 end
565
566 private
567
568 def init
569 @from = @by = @via = @with = @id = @_for = nil
570 @with = []
571 @date = nil
572 end
573
574 def set( args )
575 @from, @by, @via, @with, @id, @_for, @date = *args
576 end
577
578 def isempty?
579 @with.empty? and not (@from or @by or @via or @id or @_for or @date)
580 end
581
582 def do_accept( strategy )
583 list = []
584 list.push 'from ' + @from if @from
585 list.push 'by ' + @by if @by
586 list.push 'via ' + @via if @via
587 @with.each do |i|
588 list.push 'with ' + i
589 end
590 list.push 'id ' + @id if @id
591 list.push 'for <' + @_for + '>' if @_for
592
593 first = true
594 list.each do |i|
595 strategy.space unless first
596 strategy.meta i
597 first = false
598 end
599 if @date
600 strategy.meta ';'
601 strategy.space
602 strategy.meta time2str(@date)
603 end
604 end
605
606 end
607
608
609 class KeywordsHeader < StructuredHeader
610
611 PARSE_TYPE = :KEYWORDS
612
613 def keys
614 ensure_parsed
615 @keys
616 end
617
618 private
619
620 def init
621 @keys = []
622 end
623
624 def set( a )
625 @keys = a
626 end
627
628 def isempty?
629 @keys.empty?
630 end
631
632 def do_accept( strategy )
633 first = true
634 @keys.each do |i|
635 if first
636 first = false
637 else
638 strategy.meta ','
639 end
640 strategy.meta i
641 end
642 end
643
644 end
645
646
647 class EncryptedHeader < StructuredHeader
648
649 PARSE_TYPE = :ENCRYPTED
650
651 def encrypter
652 ensure_parsed
653 @encrypter
654 end
655
656 def encrypter=( arg )
657 ensure_parsed
658 @encrypter = arg
659 end
660
661 def keyword
662 ensure_parsed
663 @keyword
664 end
665
666 def keyword=( arg )
667 ensure_parsed
668 @keyword = arg
669 end
670
671 private
672
673 def init
674 @encrypter = nil
675 @keyword = nil
676 end
677
678 def set( args )
679 @encrypter, @keyword = args
680 end
681
682 def isempty?
683 not (@encrypter or @keyword)
684 end
685
686 def do_accept( strategy )
687 if @key
688 strategy.meta @encrypter + ','
689 strategy.space
690 strategy.meta @keyword
691 else
692 strategy.meta @encrypter
693 end
694 end
695
696 end
697
698
699 class MimeVersionHeader < StructuredHeader
700
701 PARSE_TYPE = :MIMEVERSION
702
703 def major
704 ensure_parsed
705 @major
706 end
707
708 def major=( arg )
709 ensure_parsed
710 @major = arg
711 end
712
713 def minor
714 ensure_parsed
715 @minor
716 end
717
718 def minor=( arg )
719 ensure_parsed
720 @minor = arg
721 end
722
723 def version
724 sprintf('%d.%d', major, minor)
725 end
726
727 private
728
729 def init
730 @major = nil
731 @minor = nil
732 end
733
734 def set( args )
735 @major, @minor = *args
736 end
737
738 def isempty?
739 not (@major or @minor)
740 end
741
742 def do_accept( strategy )
743 strategy.meta sprintf('%d.%d', @major, @minor)
744 end
745
746 end
747
748
749 class ContentTypeHeader < StructuredHeader
750
751 PARSE_TYPE = :CTYPE
752
753 def main_type
754 ensure_parsed
755 @main
756 end
757
758 def main_type=( arg )
759 ensure_parsed
760 @main = arg.downcase
761 end
762
763 def sub_type
764 ensure_parsed
765 @sub
766 end
767
768 def sub_type=( arg )
769 ensure_parsed
770 @sub = arg.downcase
771 end
772
773 def content_type
774 ensure_parsed
775 @sub ? sprintf('%s/%s', @main, @sub) : @main
776 end
777
778 def params
779 ensure_parsed
780 unless @params.blank?
781 @params.each do |k, v|
782 @params[k] = unquote(v)
783 end
784 end
785 @params
786 end
787
788 def []( key )
789 ensure_parsed
790 @params and unquote(@params[key])
791 end
792
793 def []=( key, val )
794 ensure_parsed
795 (@params ||= {})[key] = val
796 end
797
798 private
799
800 def init
801 @main = @sub = @params = nil
802 end
803
804 def set( args )
805 @main, @sub, @params = *args
806 end
807
808 def isempty?
809 not (@main or @sub)
810 end
811
812 def do_accept( strategy )
813 if @sub
814 strategy.meta sprintf('%s/%s', @main, @sub)
815 else
816 strategy.meta @main
817 end
818 @params.each do |k,v|
819 if v
820 strategy.meta ';'
821 strategy.space
822 strategy.kv_pair k, unquote(v)
823 end
824 end
825 end
826
827 end
828
829
830 class ContentTransferEncodingHeader < StructuredHeader
831
832 PARSE_TYPE = :CENCODING
833
834 def encoding
835 ensure_parsed
836 @encoding
837 end
838
839 def encoding=( arg )
840 ensure_parsed
841 @encoding = arg
842 end
843
844 private
845
846 def init
847 @encoding = nil
848 end
849
850 def set( s )
851 @encoding = s
852 end
853
854 def isempty?
855 not @encoding
856 end
857
858 def do_accept( strategy )
859 strategy.meta @encoding.capitalize
860 end
861
862 end
863
864
865 class ContentDispositionHeader < StructuredHeader
866
867 PARSE_TYPE = :CDISPOSITION
868
869 def disposition
870 ensure_parsed
871 @disposition
872 end
873
874 def disposition=( str )
875 ensure_parsed
876 @disposition = str.downcase
877 end
878
879 def params
880 ensure_parsed
881 unless @params.blank?
882 @params.each do |k, v|
883 @params[k] = unquote(v)
884 end
885 end
886 @params
887 end
888
889 def []( key )
890 ensure_parsed
891 @params and unquote(@params[key])
892 end
893
894 def []=( key, val )
895 ensure_parsed
896 (@params ||= {})[key] = val
897 end
898
899 private
900
901 def init
902 @disposition = @params = nil
903 end
904
905 def set( args )
906 @disposition, @params = *args
907 end
908
909 def isempty?
910 not @disposition and (not @params or @params.empty?)
911 end
912
913 def do_accept( strategy )
914 strategy.meta @disposition
915 @params.each do |k,v|
916 strategy.meta ';'
917 strategy.space
918 strategy.kv_pair k, unquote(v)
919 end
920 end
921
922 end
923
924
925 class HeaderField # redefine
926
927 FNAME_TO_CLASS = {
928 'date' => DateTimeHeader,
929 'resent-date' => DateTimeHeader,
930 'to' => AddressHeader,
931 'cc' => AddressHeader,
932 'bcc' => AddressHeader,
933 'from' => AddressHeader,
934 'reply-to' => AddressHeader,
935 'resent-to' => AddressHeader,
936 'resent-cc' => AddressHeader,
937 'resent-bcc' => AddressHeader,
938 'resent-from' => AddressHeader,
939 'resent-reply-to' => AddressHeader,
940 'sender' => SingleAddressHeader,
941 'resent-sender' => SingleAddressHeader,
942 'return-path' => ReturnPathHeader,
943 'message-id' => MessageIdHeader,
944 'resent-message-id' => MessageIdHeader,
945 'in-reply-to' => ReferencesHeader,
946 'received' => ReceivedHeader,
947 'references' => ReferencesHeader,
948 'keywords' => KeywordsHeader,
949 'encrypted' => EncryptedHeader,
950 'mime-version' => MimeVersionHeader,
951 'content-type' => ContentTypeHeader,
952 'content-transfer-encoding' => ContentTransferEncodingHeader,
953 'content-disposition' => ContentDispositionHeader,
954 'content-id' => MessageIdHeader,
955 'subject' => UnstructuredHeader,
956 'comments' => UnstructuredHeader,
957 'content-description' => UnstructuredHeader
958 }
959
960 end
961
962 end # module TMail
@@ -0,0 +1,9
1 #:stopdoc:
2 # This is here for Rolls.
3 # Rolls uses this instead of lib/tmail.rb.
4
5 require 'tmail/version'
6 require 'tmail/mail'
7 require 'tmail/mailbox'
8 require 'tmail/core_extensions'
9 #:startdoc: No newline at end of file
This diff has been collapsed as it changes many lines, (1162 lines changed) Show them Hide them
@@ -0,0 +1,1162
1 =begin rdoc
2
3 = interface.rb Provides an interface to the TMail object
4
5 =end
6 #--
7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
30 #++
31
32 # TMail::Mail objects get accessed primarily through the methods in this file.
33 #
34 #
35
36 require 'tmail/utils'
37
38 module TMail
39
40 class Mail
41
42 # Allows you to query the mail object with a string to get the contents
43 # of the field you want.
44 #
45 # Returns a string of the exact contents of the field
46 #
47 # mail.from = "mikel <mikel@lindsaar.net>"
48 # mail.header_string("From") #=> "mikel <mikel@lindsaar.net>"
49 def header_string( name, default = nil )
50 h = @header[name.downcase] or return default
51 h.to_s
52 end
53
54 #:stopdoc:
55 #--
56 #== Attributes
57
58 include TextUtils
59
60 def set_string_array_attr( key, strs )
61 strs.flatten!
62 if strs.empty?
63 @header.delete key.downcase
64 else
65 store key, strs.join(', ')
66 end
67 strs
68 end
69 private :set_string_array_attr
70
71 def set_string_attr( key, str )
72 if str
73 store key, str
74 else
75 @header.delete key.downcase
76 end
77 str
78 end
79 private :set_string_attr
80
81 def set_addrfield( name, arg )
82 if arg
83 h = HeaderField.internal_new(name, @config)
84 h.addrs.replace [arg].flatten
85 @header[name] = h
86 else
87 @header.delete name
88 end
89 arg
90 end
91 private :set_addrfield
92
93 def addrs2specs( addrs )
94 return nil unless addrs
95 list = addrs.map {|addr|
96 if addr.address_group?
97 then addr.map {|a| a.spec }
98 else addr.spec
99 end
100 }.flatten
101 return nil if list.empty?
102 list
103 end
104 private :addrs2specs
105
106 #:startdoc:
107
108 #== Date and Time methods
109
110 # Returns the date of the email message as per the "date" header value or returns
111 # nil by default (if no date field exists).
112 #
113 # You can also pass whatever default you want into this method and it will return
114 # that instead of nil if there is no date already set.
115 def date( default = nil )
116 if h = @header['date']
117 h.date
118 else
119 default
120 end
121 end
122
123 # Destructively sets the date of the mail object with the passed Time instance,
124 # returns a Time instance set to the date/time of the mail
125 #
126 # Example:
127 #
128 # now = Time.now
129 # mail.date = now
130 # mail.date #=> Sat Nov 03 18:47:50 +1100 2007
131 # mail.date.class #=> Time
132 def date=( time )
133 if time
134 store 'Date', time2str(time)
135 else
136 @header.delete 'date'
137 end
138 time
139 end
140
141 # Returns the time of the mail message formatted to your taste using a
142 # strftime format string. If no date set returns nil by default or whatever value
143 # you pass as the second optional parameter.
144 #
145 # time = Time.now # (on Nov 16 2007)
146 # mail.date = time
147 # mail.strftime("%D") #=> "11/16/07"
148 def strftime( fmt, default = nil )
149 if t = date
150 t.strftime(fmt)
151 else
152 default
153 end
154 end
155
156 #== Destination methods
157
158 # Return a TMail::Addresses instance for each entry in the "To:" field of the mail object header.
159 #
160 # If the "To:" field does not exist, will return nil by default or the value you
161 # pass as the optional parameter.
162 #
163 # Example:
164 #
165 # mail = TMail::Mail.new
166 # mail.to_addrs #=> nil
167 # mail.to_addrs([]) #=> []
168 # mail.to = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
169 # mail.to_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
170 def to_addrs( default = nil )
171 if h = @header['to']
172 h.addrs
173 else
174 default
175 end
176 end
177
178 # Return a TMail::Addresses instance for each entry in the "Cc:" field of the mail object header.
179 #
180 # If the "Cc:" field does not exist, will return nil by default or the value you
181 # pass as the optional parameter.
182 #
183 # Example:
184 #
185 # mail = TMail::Mail.new
186 # mail.cc_addrs #=> nil
187 # mail.cc_addrs([]) #=> []
188 # mail.cc = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
189 # mail.cc_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
190 def cc_addrs( default = nil )
191 if h = @header['cc']
192 h.addrs
193 else
194 default
195 end
196 end
197
198 # Return a TMail::Addresses instance for each entry in the "Bcc:" field of the mail object header.
199 #
200 # If the "Bcc:" field does not exist, will return nil by default or the value you
201 # pass as the optional parameter.
202 #
203 # Example:
204 #
205 # mail = TMail::Mail.new
206 # mail.bcc_addrs #=> nil
207 # mail.bcc_addrs([]) #=> []
208 # mail.bcc = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
209 # mail.bcc_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
210 def bcc_addrs( default = nil )
211 if h = @header['bcc']
212 h.addrs
213 else
214 default
215 end
216 end
217
218 # Destructively set the to field of the "To:" header to equal the passed in string.
219 #
220 # TMail will parse your contents and turn each valid email address into a TMail::Address
221 # object before assigning it to the mail message.
222 #
223 # Example:
224 #
225 # mail = TMail::Mail.new
226 # mail.to = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
227 # mail.to_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
228 def to_addrs=( arg )
229 set_addrfield 'to', arg
230 end
231
232 # Destructively set the to field of the "Cc:" header to equal the passed in string.
233 #
234 # TMail will parse your contents and turn each valid email address into a TMail::Address
235 # object before assigning it to the mail message.
236 #
237 # Example:
238 #
239 # mail = TMail::Mail.new
240 # mail.cc = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
241 # mail.cc_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
242 def cc_addrs=( arg )
243 set_addrfield 'cc', arg
244 end
245
246 # Destructively set the to field of the "Bcc:" header to equal the passed in string.
247 #
248 # TMail will parse your contents and turn each valid email address into a TMail::Address
249 # object before assigning it to the mail message.
250 #
251 # Example:
252 #
253 # mail = TMail::Mail.new
254 # mail.bcc = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
255 # mail.bcc_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
256 def bcc_addrs=( arg )
257 set_addrfield 'bcc', arg
258 end
259
260 # Returns who the email is to as an Array of email addresses as opposed to an Array of
261 # TMail::Address objects which is what Mail#to_addrs returns
262 #
263 # Example:
264 #
265 # mail = TMail::Mail.new
266 # mail.to = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
267 # mail.to #=> ["mikel@me.org", "mikel@you.org"]
268 def to( default = nil )
269 addrs2specs(to_addrs(nil)) || default
270 end
271
272 # Returns who the email cc'd as an Array of email addresses as opposed to an Array of
273 # TMail::Address objects which is what Mail#to_addrs returns
274 #
275 # Example:
276 #
277 # mail = TMail::Mail.new
278 # mail.cc = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
279 # mail.cc #=> ["mikel@me.org", "mikel@you.org"]
280 def cc( default = nil )
281 addrs2specs(cc_addrs(nil)) || default
282 end
283
284 # Returns who the email bcc'd as an Array of email addresses as opposed to an Array of
285 # TMail::Address objects which is what Mail#to_addrs returns
286 #
287 # Example:
288 #
289 # mail = TMail::Mail.new
290 # mail.bcc = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
291 # mail.bcc #=> ["mikel@me.org", "mikel@you.org"]
292 def bcc( default = nil )
293 addrs2specs(bcc_addrs(nil)) || default
294 end
295
296 # Destructively sets the "To:" field to the passed array of strings (which should be valid
297 # email addresses)
298 #
299 # Example:
300 #
301 # mail = TMail::Mail.new
302 # mail.to = ["mikel@abc.com", "Mikel <mikel@xyz.com>"]
303 # mail.to #=> ["mikel@abc.org", "mikel@xyz.org"]
304 # mail['to'].to_s #=> "mikel@abc.com, Mikel <mikel@xyz.com>"
305 def to=( *strs )
306 set_string_array_attr 'To', strs
307 end
308
309 # Destructively sets the "Cc:" field to the passed array of strings (which should be valid
310 # email addresses)
311 #
312 # Example:
313 #
314 # mail = TMail::Mail.new
315 # mail.cc = ["mikel@abc.com", "Mikel <mikel@xyz.com>"]
316 # mail.cc #=> ["mikel@abc.org", "mikel@xyz.org"]
317 # mail['cc'].to_s #=> "mikel@abc.com, Mikel <mikel@xyz.com>"
318 def cc=( *strs )
319 set_string_array_attr 'Cc', strs
320 end
321
322 # Destructively sets the "Bcc:" field to the passed array of strings (which should be valid
323 # email addresses)
324 #
325 # Example:
326 #
327 # mail = TMail::Mail.new
328 # mail.bcc = ["mikel@abc.com", "Mikel <mikel@xyz.com>"]
329 # mail.bcc #=> ["mikel@abc.org", "mikel@xyz.org"]
330 # mail['bcc'].to_s #=> "mikel@abc.com, Mikel <mikel@xyz.com>"
331 def bcc=( *strs )
332 set_string_array_attr 'Bcc', strs
333 end
334
335 #== Originator methods
336
337 # Return a TMail::Addresses instance for each entry in the "From:" field of the mail object header.
338 #
339 # If the "From:" field does not exist, will return nil by default or the value you
340 # pass as the optional parameter.
341 #
342 # Example:
343 #
344 # mail = TMail::Mail.new
345 # mail.from_addrs #=> nil
346 # mail.from_addrs([]) #=> []
347 # mail.from = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
348 # mail.from_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
349 def from_addrs( default = nil )
350 if h = @header['from']
351 h.addrs
352 else
353 default
354 end
355 end
356
357 # Destructively set the to value of the "From:" header to equal the passed in string.
358 #
359 # TMail will parse your contents and turn each valid email address into a TMail::Address
360 # object before assigning it to the mail message.
361 #
362 # Example:
363 #
364 # mail = TMail::Mail.new
365 # mail.from_addrs = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
366 # mail.from_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
367 def from_addrs=( arg )
368 set_addrfield 'from', arg
369 end
370
371 # Returns who the email is from as an Array of email address strings instead to an Array of
372 # TMail::Address objects which is what Mail#from_addrs returns
373 #
374 # Example:
375 #
376 # mail = TMail::Mail.new
377 # mail.from = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
378 # mail.from #=> ["mikel@me.org", "mikel@you.org"]
379 def from( default = nil )
380 addrs2specs(from_addrs(nil)) || default
381 end
382
383 # Destructively sets the "From:" field to the passed array of strings (which should be valid
384 # email addresses)
385 #
386 # Example:
387 #
388 # mail = TMail::Mail.new
389 # mail.from = ["mikel@abc.com", "Mikel <mikel@xyz.com>"]
390 # mail.from #=> ["mikel@abc.org", "mikel@xyz.org"]
391 # mail['from'].to_s #=> "mikel@abc.com, Mikel <mikel@xyz.com>"
392 def from=( *strs )
393 set_string_array_attr 'From', strs
394 end
395
396 # Returns the "friendly" human readable part of the address
397 #
398 # Example:
399 #
400 # mail = TMail::Mail.new
401 # mail.from = "Mikel Lindsaar <mikel@abc.com>"
402 # mail.friendly_from #=> "Mikel Lindsaar"
403 def friendly_from( default = nil )
404 h = @header['from']
405 a, = h.addrs
406 return default unless a
407 return a.phrase if a.phrase
408 return h.comments.join(' ') unless h.comments.empty?
409 a.spec
410 end
411
412 # Return a TMail::Addresses instance for each entry in the "Reply-To:" field of the mail object header.
413 #
414 # If the "Reply-To:" field does not exist, will return nil by default or the value you
415 # pass as the optional parameter.
416 #
417 # Example:
418 #
419 # mail = TMail::Mail.new
420 # mail.reply_to_addrs #=> nil
421 # mail.reply_to_addrs([]) #=> []
422 # mail.reply_to = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
423 # mail.reply_to_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
424 def reply_to_addrs( default = nil )
425 if h = @header['reply-to']
426 h.addrs.blank? ? default : h.addrs
427 else
428 default
429 end
430 end
431
432 # Destructively set the to value of the "Reply-To:" header to equal the passed in argument.
433 #
434 # TMail will parse your contents and turn each valid email address into a TMail::Address
435 # object before assigning it to the mail message.
436 #
437 # Example:
438 #
439 # mail = TMail::Mail.new
440 # mail.reply_to_addrs = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
441 # mail.reply_to_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
442 def reply_to_addrs=( arg )
443 set_addrfield 'reply-to', arg
444 end
445
446 # Returns who the email is from as an Array of email address strings instead to an Array of
447 # TMail::Address objects which is what Mail#reply_to_addrs returns
448 #
449 # Example:
450 #
451 # mail = TMail::Mail.new
452 # mail.reply_to = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
453 # mail.reply_to #=> ["mikel@me.org", "mikel@you.org"]
454 def reply_to( default = nil )
455 addrs2specs(reply_to_addrs(nil)) || default
456 end
457
458 # Destructively sets the "Reply-To:" field to the passed array of strings (which should be valid
459 # email addresses)
460 #
461 # Example:
462 #
463 # mail = TMail::Mail.new
464 # mail.reply_to = ["mikel@abc.com", "Mikel <mikel@xyz.com>"]
465 # mail.reply_to #=> ["mikel@abc.org", "mikel@xyz.org"]
466 # mail['reply_to'].to_s #=> "mikel@abc.com, Mikel <mikel@xyz.com>"
467 def reply_to=( *strs )
468 set_string_array_attr 'Reply-To', strs
469 end
470
471 # Return a TMail::Addresses instance of the "Sender:" field of the mail object header.
472 #
473 # If the "Sender:" field does not exist, will return nil by default or the value you
474 # pass as the optional parameter.
475 #
476 # Example:
477 #
478 # mail = TMail::Mail.new
479 # mail.sender #=> nil
480 # mail.sender([]) #=> []
481 # mail.sender = "Mikel <mikel@me.org>"
482 # mail.reply_to_addrs #=> [#<TMail::Address mikel@me.org>]
483 def sender_addr( default = nil )
484 f = @header['sender'] or return default
485 f.addr or return default
486 end
487
488 # Destructively set the to value of the "Sender:" header to equal the passed in argument.
489 #
490 # TMail will parse your contents and turn each valid email address into a TMail::Address
491 # object before assigning it to the mail message.
492 #
493 # Example:
494 #
495 # mail = TMail::Mail.new
496 # mail.sender_addrs = "Mikel <mikel@me.org>, another Mikel <mikel@you.org>"
497 # mail.sender_addrs #=> [#<TMail::Address mikel@me.org>, #<TMail::Address mikel@you.org>]
498 def sender_addr=( addr )
499 if addr
500 h = HeaderField.internal_new('sender', @config)
501 h.addr = addr
502 @header['sender'] = h
503 else
504 @header.delete 'sender'
505 end
506 addr
507 end
508
509 # Returns who the sender of this mail is as string instead to an Array of
510 # TMail::Address objects which is what Mail#sender_addr returns
511 #
512 # Example:
513 #
514 # mail = TMail::Mail.new
515 # mail.sender = "Mikel <mikel@me.org>"
516 # mail.sender #=> "mikel@me.org"
517 def sender( default = nil )
518 f = @header['sender'] or return default
519 a = f.addr or return default
520 a.spec
521 end
522
523 # Destructively sets the "Sender:" field to the passed string (which should be a valid
524 # email address)
525 #
526 # Example:
527 #
528 # mail = TMail::Mail.new
529 # mail.sender = "mikel@abc.com"
530 # mail.sender #=> "mikel@abc.org"
531 # mail['sender'].to_s #=> "mikel@abc.com"
532 def sender=( str )
533 set_string_attr 'Sender', str
534 end
535
536 #== Subject methods
537
538 # Returns the subject of the mail instance.
539 #
540 # If the subject field does not exist, returns nil by default or you can pass in as
541 # the parameter for what you want the default value to be.
542 #
543 # Example:
544 #
545 # mail = TMail::Mail.new
546 # mail.subject #=> nil
547 # mail.subject("") #=> ""
548 # mail.subject = "Hello"
549 # mail.subject #=> "Hello"
550 def subject( default = nil )
551 if h = @header['subject']
552 h.body
553 else
554 default
555 end
556 end
557 alias quoted_subject subject
558
559 # Destructively sets the passed string as the subject of the mail message.
560 #
561 # Example
562 #
563 # mail = TMail::Mail.new
564 # mail.subject #=> "This subject"
565 # mail.subject = "Another subject"
566 # mail.subject #=> "Another subject"
567 def subject=( str )
568 set_string_attr 'Subject', str
569 end
570
571 #== Message Identity & Threading Methods
572
573 # Returns the message ID for this mail object instance.
574 #
575 # If the message_id field does not exist, returns nil by default or you can pass in as
576 # the parameter for what you want the default value to be.
577 #
578 # Example:
579 #
580 # mail = TMail::Mail.new
581 # mail.message_id #=> nil
582 # mail.message_id(TMail.new_message_id) #=> "<47404c5326d9c_2ad4fbb80161@baci.local.tmail>"
583 # mail.message_id = TMail.new_message_id
584 # mail.message_id #=> "<47404c5326d9c_2ad4fbb80161@baci.local.tmail>"
585 def message_id( default = nil )
586 if h = @header['message-id']
587 h.id || default
588 else
589 default
590 end
591 end
592
593 # Destructively sets the message ID of the mail object instance to the passed in string
594 #
595 # Invalid message IDs are ignored (silently, unless configured otherwise) and result in
596 # a nil message ID. Left and right angle brackets are required.
597 #
598 # Be warned however, that calling mail.ready_to_send will overwrite whatever value you
599 # have in this field with an automatically generated unique value.
600 #
601 # If you really want to set your own message ID and know what you are doing per the
602 # various RFCs, you can do so with the enforced_message_id= command
603 #
604 # Example:
605 #
606 # mail = TMail::Mail.new
607 # mail.message_id = "<348F04F142D69C21-291E56D292BC@xxxx.net>"
608 # mail.message_id #=> "<348F04F142D69C21-291E56D292BC@xxxx.net>"
609 # mail.message_id = "this_is_my_badly_formatted_message_id"
610 # mail.message_id #=> nil
611 def message_id=( str )
612 set_string_attr 'Message-Id', str
613 end
614
615 # Destructively sets the message ID of the mail object instance to the passed in string
616 # and also guarantees that calling #ready_to_send will not destroy what you set as the
617 # message_id
618 #
619 # Example:
620 #
621 # mail = TMail::Mail.new
622 # mail.message_id = "<348F04F142D69C21-291E56D292BC@xxxx.net>"
623 # mail.message_id #=> "<348F04F142D69C21-291E56D292BC@xxxx.net>"
624 # mail.ready_to_send
625 # mail.message_id #=> "<348F04F142D69C21-291E56D292BC@xxxx.net>"
626 def enforced_message_id=( str )
627 @message_id_enforced = true
628 self.message_id = ( str )
629 end
630
631 # Returns the "In-Reply-To:" field contents as an array of this mail instance if it exists
632 #
633 # If the in_reply_to field does not exist, returns nil by default or you can pass in as
634 # the parameter for what you want the default value to be.
635 #
636 # Example:
637 #
638 # mail = TMail::Mail.new
639 # mail.in_reply_to #=> nil
640 # mail.in_reply_to([]) #=> []
641 # TMail::Mail.load("../test/fixtures/raw_email_reply")
642 # mail.in_reply_to #=> ["<348F04F142D69C21-291E56D292BC@xxxx.net>"]
643 def in_reply_to( default = nil )
644 if h = @header['in-reply-to']
645 h.ids
646 else
647 default
648 end
649 end
650
651 # Destructively sets the value of the "In-Reply-To:" field of an email.
652 #
653 # Accepts an array of a single string of a message id
654 #
655 # Example:
656 #
657 # mail = TMail::Mail.new
658 # mail.in_reply_to = ["<348F04F142D69C21-291E56D292BC@xxxx.net>"]
659 # mail.in_reply_to #=> ["<348F04F142D69C21-291E56D292BC@xxxx.net>"]
660 def in_reply_to=( *idstrs )
661 set_string_array_attr 'In-Reply-To', idstrs
662 end
663
664 # Returns the references of this email (prior messages relating to this message)
665 # as an array of message ID strings. Useful when you are trying to thread an
666 # email.
667 #
668 # If the references field does not exist, returns nil by default or you can pass in as
669 # the parameter for what you want the default value to be.
670 #
671 # Example:
672 #
673 # mail = TMail::Mail.new
674 # mail.references #=> nil
675 # mail.references([]) #=> []
676 # mail = TMail::Mail.load("../test/fixtures/raw_email_reply")
677 # mail.references #=> ["<473FF3B8.9020707@xxx.org>", "<348F04F142D69C21-291E56D292BC@xxxx.net>"]
678 def references( default = nil )
679 if h = @header['references']
680 h.refs
681 else
682 default
683 end
684 end
685
686 # Destructively sets the value of the "References:" field of an email.
687 #
688 # Accepts an array of strings of message IDs
689 #
690 # Example:
691 #
692 # mail = TMail::Mail.new
693 # mail.references = ["<348F04F142D69C21-291E56D292BC@xxxx.net>"]
694 # mail.references #=> ["<348F04F142D69C21-291E56D292BC@xxxx.net>"]
695 def references=( *strs )
696 set_string_array_attr 'References', strs
697 end
698
699 #== MIME header methods
700
701 # Returns the listed MIME version of this email from the "Mime-Version:" header field
702 #
703 # If the mime_version field does not exist, returns nil by default or you can pass in as
704 # the parameter for what you want the default value to be.
705 #
706 # Example:
707 #
708 # mail = TMail::Mail.new
709 # mail.mime_version #=> nil
710 # mail.mime_version([]) #=> []
711 # mail = TMail::Mail.load("../test/fixtures/raw_email")
712 # mail.mime_version #=> "1.0"
713 def mime_version( default = nil )
714 if h = @header['mime-version']
715 h.version || default
716 else
717 default
718 end
719 end
720
721 def mime_version=( m, opt = nil )
722 if opt
723 if h = @header['mime-version']
724 h.major = m
725 h.minor = opt
726 else
727 store 'Mime-Version', "#{m}.#{opt}"
728 end
729 else
730 store 'Mime-Version', m
731 end
732 m
733 end
734
735 # Returns the current "Content-Type" of the mail instance.
736 #
737 # If the content_type field does not exist, returns nil by default or you can pass in as
738 # the parameter for what you want the default value to be.
739 #
740 # Example:
741 #
742 # mail = TMail::Mail.new
743 # mail.content_type #=> nil
744 # mail.content_type([]) #=> []
745 # mail = TMail::Mail.load("../test/fixtures/raw_email")
746 # mail.content_type #=> "text/plain"
747 def content_type( default = nil )
748 if h = @header['content-type']
749 h.content_type || default
750 else
751 default
752 end
753 end
754
755 # Returns the current main type of the "Content-Type" of the mail instance.
756 #
757 # If the content_type field does not exist, returns nil by default or you can pass in as
758 # the parameter for what you want the default value to be.
759 #
760 # Example:
761 #
762 # mail = TMail::Mail.new
763 # mail.main_type #=> nil
764 # mail.main_type([]) #=> []
765 # mail = TMail::Mail.load("../test/fixtures/raw_email")
766 # mail.main_type #=> "text"
767 def main_type( default = nil )
768 if h = @header['content-type']
769 h.main_type || default
770 else
771 default
772 end
773 end
774
775 # Returns the current sub type of the "Content-Type" of the mail instance.
776 #
777 # If the content_type field does not exist, returns nil by default or you can pass in as
778 # the parameter for what you want the default value to be.
779 #
780 # Example:
781 #
782 # mail = TMail::Mail.new
783 # mail.sub_type #=> nil
784 # mail.sub_type([]) #=> []
785 # mail = TMail::Mail.load("../test/fixtures/raw_email")
786 # mail.sub_type #=> "plain"
787 def sub_type( default = nil )
788 if h = @header['content-type']
789 h.sub_type || default
790 else
791 default
792 end
793 end
794
795 # Destructively sets the "Content-Type:" header field of this mail object
796 #
797 # Allows you to set the main type, sub type as well as parameters to the field.
798 # The main type and sub type need to be a string.
799 #
800 # The optional params hash can be passed with keys as symbols and values as a string,
801 # or strings as keys and values.
802 #
803 # Example:
804 #
805 # mail = TMail::Mail.new
806 # mail.set_content_type("text", "plain")
807 # mail.to_s #=> "Content-Type: text/plain\n\n"
808 #
809 # mail.set_content_type("text", "plain", {:charset => "EUC-KR", :format => "flowed"})
810 # mail.to_s #=> "Content-Type: text/plain; charset=EUC-KR; format=flowed\n\n"
811 #
812 # mail.set_content_type("text", "plain", {"charset" => "EUC-KR", "format" => "flowed"})
813 # mail.to_s #=> "Content-Type: text/plain; charset=EUC-KR; format=flowed\n\n"
814 def set_content_type( str, sub = nil, param = nil )
815 if sub
816 main, sub = str, sub
817 else
818 main, sub = str.split(%r</>, 2)
819 raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
820 end
821 if h = @header['content-type']
822 h.main_type = main
823 h.sub_type = sub
824 h.params.clear
825 else
826 store 'Content-Type', "#{main}/#{sub}"
827 end
828 @header['content-type'].params.replace param if param
829 str
830 end
831
832 alias content_type= set_content_type
833
834 # Returns the named type parameter as a string, from the "Content-Type:" header.
835 #
836 # Example:
837 #
838 # mail = TMail::Mail.new
839 # mail.type_param("charset") #=> nil
840 # mail.type_param("charset", []) #=> []
841 # mail.set_content_type("text", "plain", {:charset => "EUC-KR", :format => "flowed"})
842 # mail.type_param("charset") #=> "EUC-KR"
843 # mail.type_param("format") #=> "flowed"
844 def type_param( name, default = nil )
845 if h = @header['content-type']
846 h[name] || default
847 else
848 default
849 end
850 end
851
852 # Returns the character set of the email. Returns nil if no encoding set or returns
853 # whatever default you pass as a parameter - note passing the parameter does NOT change
854 # the mail object in any way.
855 #
856 # Example:
857 #
858 # mail = TMail::Mail.load("path_to/utf8_email")
859 # mail.charset #=> "UTF-8"
860 #
861 # mail = TMail::Mail.new
862 # mail.charset #=> nil
863 # mail.charset("US-ASCII") #=> "US-ASCII"
864 def charset( default = nil )
865 if h = @header['content-type']
866 h['charset'] or default
867 else
868 mime_version_charset || default
869 end
870 end
871
872 # some weird emails come with the charset specified in the mime-version header:
873 #
874 # #<TMail::MimeVersionHeader "1.0\n charset=\"gb2312\"">
875 #
876 def mime_version_charset
877 if header['mime-version'].inspect =~ /charset=('|\\")?([^\\"']+)/
878 $2
879 end
880 end
881
882 # Destructively sets the character set used by this mail object to the passed string, you
883 # should note though that this does nothing to the mail body, just changes the header
884 # value, you will need to transliterate the body as well to match whatever you put
885 # in this header value if you are changing character sets.
886 #
887 # Example:
888 #
889 # mail = TMail::Mail.new
890 # mail.charset #=> nil
891 # mail.charset = "UTF-8"
892 # mail.charset #=> "UTF-8"
893 def charset=( str )
894 if str
895 if h = @header[ 'content-type' ]
896 h['charset'] = str
897 else
898 store 'Content-Type', "text/plain; charset=#{str}"
899 end
900 end
901 str
902 end
903
904 # Returns the transfer encoding of the email. Returns nil if no encoding set or returns
905 # whatever default you pass as a parameter - note passing the parameter does NOT change
906 # the mail object in any way.
907 #
908 # Example:
909 #
910 # mail = TMail::Mail.load("path_to/base64_encoded_email")
911 # mail.transfer_encoding #=> "base64"
912 #
913 # mail = TMail::Mail.new
914 # mail.transfer_encoding #=> nil
915 # mail.transfer_encoding("base64") #=> "base64"
916 def transfer_encoding( default = nil )
917 if h = @header['content-transfer-encoding']
918 h.encoding || default
919 else
920 default
921 end
922 end
923
924 # Destructively sets the transfer encoding of the mail object to the passed string, you
925 # should note though that this does nothing to the mail body, just changes the header
926 # value, you will need to encode or decode the body as well to match whatever you put
927 # in this header value.
928 #
929 # Example:
930 #
931 # mail = TMail::Mail.new
932 # mail.transfer_encoding #=> nil
933 # mail.transfer_encoding = "base64"
934 # mail.transfer_encoding #=> "base64"
935 def transfer_encoding=( str )
936 set_string_attr 'Content-Transfer-Encoding', str
937 end
938
939 alias encoding transfer_encoding
940 alias encoding= transfer_encoding=
941 alias content_transfer_encoding transfer_encoding
942 alias content_transfer_encoding= transfer_encoding=
943
944 # Returns the content-disposition of the mail object, returns nil or the passed
945 # default value if given
946 #
947 # Example:
948 #
949 # mail = TMail::Mail.load("path_to/raw_mail_with_attachment")
950 # mail.disposition #=> "attachment"
951 #
952 # mail = TMail::Mail.load("path_to/plain_simple_email")
953 # mail.disposition #=> nil
954 # mail.disposition(false) #=> false
955 def disposition( default = nil )
956 if h = @header['content-disposition']
957 h.disposition || default
958 else
959 default
960 end
961 end
962
963 alias content_disposition disposition
964
965 # Allows you to set the content-disposition of the mail object. Accepts a type
966 # and a hash of parameters.
967 #
968 # Example:
969 #
970 # mail.set_disposition("attachment", {:filename => "test.rb"})
971 # mail.disposition #=> "attachment"
972 # mail['content-disposition'].to_s #=> "attachment; filename=test.rb"
973 def set_disposition( str, params = nil )
974 if h = @header['content-disposition']
975 h.disposition = str
976 h.params.clear
977 else
978 store('Content-Disposition', str)
979 h = @header['content-disposition']
980 end
981 h.params.replace params if params
982 end
983
984 alias disposition= set_disposition
985 alias set_content_disposition set_disposition
986 alias content_disposition= set_disposition
987
988 # Returns the value of a parameter in an existing content-disposition header
989 #
990 # Example:
991 #
992 # mail.set_disposition("attachment", {:filename => "test.rb"})
993 # mail['content-disposition'].to_s #=> "attachment; filename=test.rb"
994 # mail.disposition_param("filename") #=> "test.rb"
995 # mail.disposition_param("missing_param_key") #=> nil
996 # mail.disposition_param("missing_param_key", false) #=> false
997 # mail.disposition_param("missing_param_key", "Nothing to see here") #=> "Nothing to see here"
998 def disposition_param( name, default = nil )
999 if h = @header['content-disposition']
1000 h[name] || default
1001 else
1002 default
1003 end
1004 end
1005
1006 # Convert the Mail object's body into a Base64 encoded email
1007 # returning the modified Mail object
1008 def base64_encode!
1009 store 'Content-Transfer-Encoding', 'Base64'
1010 self.body = base64_encode
1011 end
1012
1013 # Return the result of encoding the TMail::Mail object body
1014 # without altering the current body
1015 def base64_encode
1016 Base64.folding_encode(self.body)
1017 end
1018
1019 # Convert the Mail object's body into a Base64 decoded email
1020 # returning the modified Mail object
1021 def base64_decode!
1022 if /base64/i === self.transfer_encoding('')
1023 store 'Content-Transfer-Encoding', '8bit'
1024 self.body = base64_decode
1025 end
1026 end
1027
1028 # Returns the result of decoding the TMail::Mail object body
1029 # without altering the current body
1030 def base64_decode
1031 Base64.decode(self.body, @config.strict_base64decode?)
1032 end
1033
1034 # Returns an array of each destination in the email message including to: cc: or bcc:
1035 #
1036 # Example:
1037 #
1038 # mail.to = "Mikel <mikel@lindsaar.net>"
1039 # mail.cc = "Trans <t@t.com>"
1040 # mail.bcc = "bob <bob@me.com>"
1041 # mail.destinations #=> ["mikel@lindsaar.net", "t@t.com", "bob@me.com"]
1042 def destinations( default = nil )
1043 ret = []
1044 %w( to cc bcc ).each do |nm|
1045 if h = @header[nm]
1046 h.addrs.each {|i| ret.push i.address }
1047 end
1048 end
1049 ret.empty? ? default : ret
1050 end
1051
1052 # Yields a block of destination, yielding each as a string.
1053 # (from the destinations example)
1054 # mail.each_destination { |d| puts "#{d.class}: #{d}" }
1055 # String: mikel@lindsaar.net
1056 # String: t@t.com
1057 # String: bob@me.com
1058 def each_destination( &block )
1059 destinations([]).each do |i|
1060 if Address === i
1061 yield i
1062 else
1063 i.each(&block)
1064 end
1065 end
1066 end
1067
1068 alias each_dest each_destination
1069
1070 # Returns an array of reply to addresses that the Mail object has,
1071 # or if the Mail message has no reply-to, returns an array of the
1072 # Mail objects from addresses. Else returns the default which can
1073 # either be passed as a parameter or defaults to nil
1074 #
1075 # Example:
1076 # mail.from = "Mikel <mikel@lindsaar.net>"
1077 # mail.reply_to = nil
1078 # mail.reply_addresses #=> [""]
1079 #
1080 def reply_addresses( default = nil )
1081 reply_to_addrs(nil) or from_addrs(nil) or default
1082 end
1083
1084 # Returns the "sender" field as an array -> useful to find out who to
1085 # send an error email to.
1086 def error_reply_addresses( default = nil )
1087 if s = sender(nil)
1088 [s]
1089 else
1090 from_addrs(default)
1091 end
1092 end
1093
1094 # Returns true if the Mail object is a multipart message
1095 def multipart?
1096 main_type('').downcase == 'multipart'
1097 end
1098
1099 # Creates a new email in reply to self. Sets the In-Reply-To and
1100 # References headers for you automagically.
1101 #
1102 # Example:
1103 # mail = TMail::Mail.load("my_email")
1104 # reply_email = mail.create_reply
1105 # reply_email.class #=> TMail::Mail
1106 # reply_email.references #=> ["<d3b8cf8e49f04480850c28713a1f473e@lindsaar.net>"]
1107 # reply_email.in_reply_to #=> ["<d3b8cf8e49f04480850c28713a1f473e@lindsaar.net>"]
1108 def create_reply
1109 setup_reply create_empty_mail()
1110 end
1111
1112 # Creates a new email in reply to self. Sets the In-Reply-To and
1113 # References headers for you automagically.
1114 #
1115 # Example:
1116 # mail = TMail::Mail.load("my_email")
1117 # forward_email = mail.create_forward
1118 # forward_email.class #=> TMail::Mail
1119 # forward_email.content_type #=> "multipart/mixed"
1120 # forward_email.body #=> "Attachment: (unnamed)"
1121 # forward_email.encoded #=> Returns the original email as a MIME attachment
1122 def create_forward
1123 setup_forward create_empty_mail()
1124 end
1125
1126 #:stopdoc:
1127 private
1128
1129 def create_empty_mail
1130 self.class.new(StringPort.new(''), @config)
1131 end
1132
1133 def setup_reply( mail )
1134 if tmp = reply_addresses(nil)
1135 mail.to_addrs = tmp
1136 end
1137
1138 mid = message_id(nil)
1139 tmp = references(nil) || []
1140 tmp.push mid if mid
1141 mail.in_reply_to = [mid] if mid
1142 mail.references = tmp unless tmp.empty?
1143 mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
1144 mail.mime_version = '1.0'
1145 mail
1146 end
1147
1148 def setup_forward( mail )
1149 m = Mail.new(StringPort.new(''))
1150 m.body = decoded
1151 m.set_content_type 'message', 'rfc822'
1152 m.encoding = encoding('7bit')
1153 mail.parts.push m
1154 # call encoded to reparse the message
1155 mail.encoded
1156 mail
1157 end
1158
1159 #:startdoc:
1160 end # class Mail
1161
1162 end # module TMail
@@ -0,0 +1,3
1 #:stopdoc:
2 require 'tmail/mailbox'
3 #:startdoc: No newline at end of file
This diff has been collapsed as it changes many lines, (578 lines changed) Show them Hide them
@@ -0,0 +1,578
1 =begin rdoc
2
3 = Mail class
4
5 =end
6 #--
7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
30 #++
31
32
33
34 require 'tmail/interface'
35 require 'tmail/encode'
36 require 'tmail/header'
37 require 'tmail/port'
38 require 'tmail/config'
39 require 'tmail/utils'
40 require 'tmail/attachments'
41 require 'tmail/quoting'
42 require 'socket'
43
44 module TMail
45
46 # == Mail Class
47 #
48 # Accessing a TMail object done via the TMail::Mail class. As email can be fairly complex
49 # creatures, you will find a large amount of accessor and setter methods in this class!
50 #
51 # Most of the below methods handle the header, in fact, what TMail does best is handle the
52 # header of the email object. There are only a few methods that deal directly with the body
53 # of the email, such as base64_encode and base64_decode.
54 #
55 # === Using TMail inside your code
56 #
57 # The usual way is to install the gem (see the {README}[link:/README] on how to do this) and
58 # then put at the top of your class:
59 #
60 # require 'tmail'
61 #
62 # You can then create a new TMail object in your code with:
63 #
64 # @email = TMail::Mail.new
65 #
66 # Or if you have an email as a string, you can initialize a new TMail::Mail object and get it
67 # to parse that string for you like so:
68 #
69 # @email = TMail::Mail.parse(email_text)
70 #
71 # You can also read a single email off the disk, for example:
72 #
73 # @email = TMail::Mail.load('filename.txt')
74 #
75 # Also, you can read a mailbox (usual unix mbox format) and end up with an array of TMail
76 # objects by doing something like this:
77 #
78 # # Note, we pass true as the last variable to open the mailbox read only
79 # mailbox = TMail::UNIXMbox.new("mailbox", nil, true)
80 # @emails = []
81 # mailbox.each_port { |m| @emails << TMail::Mail.new(m) }
82 #
83 class Mail
84
85 class << self
86
87 # Opens an email that has been saved out as a file by itself.
88 #
89 # This function will read a file non-destructively and then parse
90 # the contents and return a TMail::Mail object.
91 #
92 # Does not handle multiple email mailboxes (like a unix mbox) for that
93 # use the TMail::UNIXMbox class.
94 #
95 # Example:
96 # mail = TMail::Mail.load('filename')
97 #
98 def load( fname )
99 new(FilePort.new(fname))
100 end
101
102 alias load_from load
103 alias loadfrom load
104
105 # Parses an email from the supplied string and returns a TMail::Mail
106 # object.
107 #
108 # Example:
109 # require 'rubygems'; require 'tmail'
110 # email_string =<<HEREDOC
111 # To: mikel@lindsaar.net
112 # From: mikel@me.com
113 # Subject: This is a short Email
114 #
115 # Hello there Mikel!
116 #
117 # HEREDOC
118 # mail = TMail::Mail.parse(email_string)
119 # #=> #<TMail::Mail port=#<TMail::StringPort:id=0xa30ac0> bodyport=nil>
120 # mail.body
121 # #=> "Hello there Mikel!\n\n"
122 def parse( str )
123 new(StringPort.new(str))
124 end
125
126 end
127
128 def initialize( port = nil, conf = DEFAULT_CONFIG ) #:nodoc:
129 @port = port || StringPort.new
130 @config = Config.to_config(conf)
131
132 @header = {}
133 @body_port = nil
134 @body_parsed = false
135 @epilogue = ''
136 @parts = []
137
138 @port.ropen {|f|
139 parse_header f
140 parse_body f unless @port.reproducible?
141 }
142 end
143
144 # Provides access to the port this email is using to hold it's data
145 #
146 # Example:
147 # mail = TMail::Mail.parse(email_string)
148 # mail.port
149 # #=> #<TMail::StringPort:id=0xa2c952>
150 attr_reader :port
151
152 def inspect
153 "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
154 end
155
156 #
157 # to_s interfaces
158 #
159
160 public
161
162 include StrategyInterface
163
164 def write_back( eol = "\n", charset = 'e' )
165 parse_body
166 @port.wopen {|stream| encoded eol, charset, stream }
167 end
168
169 def accept( strategy )
170 with_multipart_encoding(strategy) {
171 ordered_each do |name, field|
172 next if field.empty?
173 strategy.header_name canonical(name)
174 field.accept strategy
175 strategy.puts
176 end
177 strategy.puts
178 body_port().ropen {|r|
179 strategy.write r.read
180 }
181 }
182 end
183
184 private
185
186 def canonical( name )
187 name.split(/-/).map {|s| s.capitalize }.join('-')
188 end
189
190 def with_multipart_encoding( strategy )
191 if parts().empty? # DO NOT USE @parts
192 yield
193
194 else
195 bound = ::TMail.new_boundary
196 if @header.key? 'content-type'
197 @header['content-type'].params['boundary'] = bound
198 else
199 store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
200 end
201
202 yield
203
204 parts().each do |tm|
205 strategy.puts
206 strategy.puts '--' + bound
207 tm.accept strategy
208 end
209 strategy.puts
210 strategy.puts '--' + bound + '--'
211 strategy.write epilogue()
212 end
213 end
214
215 ###
216 ### header
217 ###
218
219 public
220
221 ALLOW_MULTIPLE = {
222 'received' => true,
223 'resent-date' => true,
224 'resent-from' => true,
225 'resent-sender' => true,
226 'resent-to' => true,
227 'resent-cc' => true,
228 'resent-bcc' => true,
229 'resent-message-id' => true,
230 'comments' => true,
231 'keywords' => true
232 }
233 USE_ARRAY = ALLOW_MULTIPLE
234
235 def header
236 @header.dup
237 end
238
239 # Returns a TMail::AddressHeader object of the field you are querying.
240 # Examples:
241 # @mail['from'] #=> #<TMail::AddressHeader "mikel@test.com.au">
242 # @mail['to'] #=> #<TMail::AddressHeader "mikel@test.com.au">
243 #
244 # You can get the string value of this by passing "to_s" to the query:
245 # Example:
246 # @mail['to'].to_s #=> "mikel@test.com.au"
247 def []( key )
248 @header[key.downcase]
249 end
250
251 def sub_header(key, param)
252 (hdr = self[key]) ? hdr[param] : nil
253 end
254
255 alias fetch []
256
257 # Allows you to set or delete TMail header objects at will.
258 # Examples:
259 # @mail = TMail::Mail.new
260 # @mail['to'].to_s # => 'mikel@test.com.au'
261 # @mail['to'] = 'mikel@elsewhere.org'
262 # @mail['to'].to_s # => 'mikel@elsewhere.org'
263 # @mail.encoded # => "To: mikel@elsewhere.org\r\n\r\n"
264 # @mail['to'] = nil
265 # @mail['to'].to_s # => nil
266 # @mail.encoded # => "\r\n"
267 #
268 # Note: setting mail[] = nil actually deletes the header field in question from the object,
269 # it does not just set the value of the hash to nil
270 def []=( key, val )
271 dkey = key.downcase
272
273 if val.nil?
274 @header.delete dkey
275 return nil
276 end
277
278 case val
279 when String
280 header = new_hf(key, val)
281 when HeaderField
282 ;
283 when Array
284 ALLOW_MULTIPLE.include? dkey or
285 raise ArgumentError, "#{key}: Header must not be multiple"
286 @header[dkey] = val
287 return val
288 else
289 header = new_hf(key, val.to_s)
290 end
291 if ALLOW_MULTIPLE.include? dkey
292 (@header[dkey] ||= []).push header
293 else
294 @header[dkey] = header
295 end
296
297 val
298 end
299
300 alias store []=
301
302 # Allows you to loop through each header in the TMail::Mail object in a block
303 # Example:
304 # @mail['to'] = 'mikel@elsewhere.org'
305 # @mail['from'] = 'me@me.com'
306 # @mail.each_header { |k,v| puts "#{k} = #{v}" }
307 # # => from = me@me.com
308 # # => to = mikel@elsewhere.org
309 def each_header
310 @header.each do |key, val|
311 [val].flatten.each {|v| yield key, v }
312 end
313 end
314
315 alias each_pair each_header
316
317 def each_header_name( &block )
318 @header.each_key(&block)
319 end
320
321 alias each_key each_header_name
322
323 def each_field( &block )
324 @header.values.flatten.each(&block)
325 end
326
327 alias each_value each_field
328
329 FIELD_ORDER = %w(
330 return-path received
331 resent-date resent-from resent-sender resent-to
332 resent-cc resent-bcc resent-message-id
333 date from sender reply-to to cc bcc
334 message-id in-reply-to references
335 subject comments keywords
336 mime-version content-type content-transfer-encoding
337 content-disposition content-description
338 )
339
340 def ordered_each
341 list = @header.keys
342 FIELD_ORDER.each do |name|
343 if list.delete(name)
344 [@header[name]].flatten.each {|v| yield name, v }
345 end
346 end
347 list.each do |name|
348 [@header[name]].flatten.each {|v| yield name, v }
349 end
350 end
351
352 def clear
353 @header.clear
354 end
355
356 def delete( key )
357 @header.delete key.downcase
358 end
359
360 def delete_if
361 @header.delete_if do |key,val|
362 if Array === val
363 val.delete_if {|v| yield key, v }
364 val.empty?
365 else
366 yield key, val
367 end
368 end
369 end
370
371 def keys
372 @header.keys
373 end
374
375 def key?( key )
376 @header.key? key.downcase
377 end
378
379 def values_at( *args )
380 args.map {|k| @header[k.downcase] }.flatten
381 end
382
383 alias indexes values_at
384 alias indices values_at
385
386 private
387
388 def parse_header( f )
389 name = field = nil
390 unixfrom = nil
391
392 while line = f.gets
393 case line
394 when /\A[ \t]/ # continue from prev line
395 raise SyntaxError, 'mail is began by space' unless field
396 field << ' ' << line.strip
397
398 when /\A([^\: \t]+):\s*/ # new header line
399 add_hf name, field if field
400 name = $1
401 field = $' #.strip
402
403 when /\A\-*\s*\z/ # end of header
404 add_hf name, field if field
405 name = field = nil
406 break
407
408 when /\AFrom (\S+)/
409 unixfrom = $1
410
411 when /^charset=.*/
412
413 else
414 raise SyntaxError, "wrong mail header: '#{line.inspect}'"
415 end
416 end
417 add_hf name, field if name
418
419 if unixfrom
420 add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
421 end
422 end
423
424 def add_hf( name, field )
425 key = name.downcase
426 field = new_hf(name, field)
427
428 if ALLOW_MULTIPLE.include? key
429 (@header[key] ||= []).push field
430 else
431 @header[key] = field
432 end
433 end
434
435 def new_hf( name, field )
436 HeaderField.new(name, field, @config)
437 end
438
439 ###
440 ### body
441 ###
442
443 public
444
445 def body_port
446 parse_body
447 @body_port
448 end
449
450 def each( &block )
451 body_port().ropen {|f| f.each(&block) }
452 end
453
454 def quoted_body
455 body_port.ropen {|f| return f.read }
456 end
457
458 def quoted_body= str
459 body_port.wopen { |f| f.write str }
460 str
461 end
462
463 def body=( str )
464 # Sets the body of the email to a new (encoded) string.
465 #
466 # We also reparses the email if the body is ever reassigned, this is a performance hit, however when
467 # you assign the body, you usually want to be able to make sure that you can access the attachments etc.
468 #
469 # Usage:
470 #
471 # mail.body = "Hello, this is\nthe body text"
472 # # => "Hello, this is\nthe body"
473 # mail.body
474 # # => "Hello, this is\nthe body"
475 @body_parsed = false
476 parse_body(StringInput.new(str))
477 parse_body
478 @body_port.wopen {|f| f.write str }
479 str
480 end
481
482 alias preamble quoted_body
483 alias preamble= quoted_body=
484
485 def epilogue
486 parse_body
487 @epilogue.dup
488 end
489
490 def epilogue=( str )
491 parse_body
492 @epilogue = str
493 str
494 end
495
496 def parts
497 parse_body
498 @parts
499 end
500
501 def each_part( &block )
502 parts().each(&block)
503 end
504
505 # Returns true if the content type of this part of the email is
506 # a disposition attachment
507 def disposition_is_attachment?
508 (self['content-disposition'] && self['content-disposition'].disposition == "attachment")
509 end
510
511 # Returns true if this part's content main type is text, else returns false.
512 # By main type is meant "text/plain" is text. "text/html" is text
513 def content_type_is_text?
514 self.header['content-type'] && (self.header['content-type'].main_type != "text")
515 end
516
517 private
518
519 def parse_body( f = nil )
520 return if @body_parsed
521 if f
522 parse_body_0 f
523 else
524 @port.ropen {|f|
525 skip_header f
526 parse_body_0 f
527 }
528 end
529 @body_parsed = true
530 end
531
532 def skip_header( f )
533 while line = f.gets
534 return if /\A[\r\n]*\z/ === line
535 end
536 end
537
538 def parse_body_0( f )
539 if multipart?
540 read_multipart f
541 else
542 @body_port = @config.new_body_port(self)
543 @body_port.wopen {|w|
544 w.write f.read
545 }
546 end
547 end
548
549 def read_multipart( src )
550 bound = @header['content-type'].params['boundary'] || ::TMail.new_boundary
551 is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
552 lastbound = "--#{bound}--"
553
554 ports = [ @config.new_preamble_port(self) ]
555 begin
556 f = ports.last.wopen
557 while line = src.gets
558 if is_sep === line
559 f.close
560 break if line.strip == lastbound
561 ports.push @config.new_part_port(self)
562 f = ports.last.wopen
563 else
564 f << line
565 end
566 end
567 @epilogue = (src.read || '')
568 ensure
569 f.close if f and not f.closed?
570 end
571
572 @body_port = ports.shift
573 @parts = ports.map {|p| self.class.new(p, @config) }
574 end
575
576 end # class Mail
577
578 end # module TMail
@@ -0,0 +1,496
1 =begin rdoc
2
3 = Mailbox and Mbox interaction class
4
5 =end
6 #--
7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
30 #++
31
32 require 'tmail/port'
33 require 'socket'
34 require 'mutex_m'
35
36
37 unless [].respond_to?(:sort_by)
38 module Enumerable#:nodoc:
39 def sort_by
40 map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
41 end
42 end
43 end
44
45
46 module TMail
47
48 class MhMailbox
49
50 PORT_CLASS = MhPort
51
52 def initialize( dir )
53 edir = File.expand_path(dir)
54 raise ArgumentError, "not directory: #{dir}"\
55 unless FileTest.directory? edir
56 @dirname = edir
57 @last_file = nil
58 @last_atime = nil
59 end
60
61 def directory
62 @dirname
63 end
64
65 alias dirname directory
66
67 attr_accessor :last_atime
68
69 def inspect
70 "#<#{self.class} #{@dirname}>"
71 end
72
73 def close
74 end
75
76 def new_port
77 PORT_CLASS.new(next_file_name())
78 end
79
80 def each_port
81 mail_files().each do |path|
82 yield PORT_CLASS.new(path)
83 end
84 @last_atime = Time.now
85 end
86
87 alias each each_port
88
89 def reverse_each_port
90 mail_files().reverse_each do |path|
91 yield PORT_CLASS.new(path)
92 end
93 @last_atime = Time.now
94 end
95
96 alias reverse_each reverse_each_port
97
98 # old #each_mail returns Port
99 #def each_mail
100 # each_port do |port|
101 # yield Mail.new(port)
102 # end
103 #end
104
105 def each_new_port( mtime = nil, &block )
106 mtime ||= @last_atime
107 return each_port(&block) unless mtime
108 return unless File.mtime(@dirname) >= mtime
109
110 mail_files().each do |path|
111 yield PORT_CLASS.new(path) if File.mtime(path) > mtime
112 end
113 @last_atime = Time.now
114 end
115
116 private
117
118 def mail_files
119 Dir.entries(@dirname)\
120 .select {|s| /\A\d+\z/ === s }\
121 .map {|s| s.to_i }\
122 .sort\
123 .map {|i| "#{@dirname}/#{i}" }\
124 .select {|path| FileTest.file? path }
125 end
126
127 def next_file_name
128 unless n = @last_file
129 n = 0
130 Dir.entries(@dirname)\
131 .select {|s| /\A\d+\z/ === s }\
132 .map {|s| s.to_i }.sort\
133 .each do |i|
134 next unless FileTest.file? "#{@dirname}/#{i}"
135 n = i
136 end
137 end
138 begin
139 n += 1
140 end while FileTest.exist? "#{@dirname}/#{n}"
141 @last_file = n
142
143 "#{@dirname}/#{n}"
144 end
145
146 end # MhMailbox
147
148 MhLoader = MhMailbox
149
150
151 class UNIXMbox
152
153 class << self
154 alias newobj new
155 end
156
157 # Creates a new mailbox object that you can iterate through to collect the
158 # emails from with "each_port".
159 #
160 # You need to pass it a filename of a unix mailbox format file, the format of this
161 # file can be researched at this page at {wikipedia}[link:http://en.wikipedia.org/wiki/Mbox]
162 #
163 # ==== Parameters
164 #
165 # +filename+: The filename of the mailbox you want to open
166 #
167 # +tmpdir+: Can be set to override TMail using the system environment's temp dir. TMail will first
168 # use the temp dir specified by you (if any) or then the temp dir specified in the Environment's TEMP
169 # value then the value in the Environment's TMP value or failing all of the above, '/tmp'
170 #
171 # +readonly+: If set to false, each email you take from the mail box will be removed from the mailbox.
172 # default is *false* - ie, it *WILL* truncate your mailbox file to ZERO once it has read the emails out.
173 #
174 # ==== Options:
175 #
176 # None
177 #
178 # ==== Examples:
179 #
180 # # First show using readonly true:
181 #
182 # require 'ftools'
183 # File.size("../test/fixtures/mailbox")
184 # #=> 20426
185 #
186 # mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox", nil, true)
187 # #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=true.....>
188 #
189 # mailbox.each_port do |port|
190 # mail = TMail::Mail.new(port)
191 # puts mail.subject
192 # end
193 # #Testing mailbox 1
194 # #Testing mailbox 2
195 # #Testing mailbox 3
196 # #Testing mailbox 4
197 # require 'ftools'
198 # File.size?("../test/fixtures/mailbox")
199 # #=> 20426
200 #
201 # # Now show with readonly set to the default false
202 #
203 # mailbox = TMail::UNIXMbox.new("../test/fixtures/mailbox")
204 # #=> #<TMail::UNIXMbox:0x14a2aa8 @readonly=false.....>
205 #
206 # mailbox.each_port do |port|
207 # mail = TMail::Mail.new(port)
208 # puts mail.subject
209 # end
210 # #Testing mailbox 1
211 # #Testing mailbox 2
212 # #Testing mailbox 3
213 # #Testing mailbox 4
214 #
215 # File.size?("../test/fixtures/mailbox")
216 # #=> nil
217 def UNIXMbox.new( filename, tmpdir = nil, readonly = false )
218 tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
219 newobj(filename, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
220 end
221
222 def UNIXMbox.lock( fname )
223 begin
224 f = File.open(fname, 'r+')
225 f.flock File::LOCK_EX
226 yield f
227 ensure
228 f.flock File::LOCK_UN
229 f.close if f and not f.closed?
230 end
231 end
232
233 def UNIXMbox.static_new( fname, dir, readonly = false )
234 newobj(fname, dir, readonly, true)
235 end
236
237 def initialize( fname, mhdir, readonly, static )
238 @filename = fname
239 @readonly = readonly
240 @closed = false
241
242 Dir.mkdir mhdir
243 @real = MhMailbox.new(mhdir)
244 @finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
245 ObjectSpace.define_finalizer self, @finalizer
246 end
247
248 def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
249 lambda {
250 if writeback_p
251 lock(mboxfile) {|f|
252 mh.each_port do |port|
253 f.puts create_from_line(port)
254 port.ropen {|r|
255 f.puts r.read
256 }
257 end
258 }
259 end
260 if cleanup_p
261 Dir.foreach(mh.dirname) do |fname|
262 next if /\A\.\.?\z/ === fname
263 File.unlink "#{mh.dirname}/#{fname}"
264 end
265 Dir.rmdir mh.dirname
266 end
267 }
268 end
269
270 # make _From line
271 def UNIXMbox.create_from_line( port )
272 sprintf 'From %s %s',
273 fromaddr(), TextUtils.time2str(File.mtime(port.filename))
274 end
275
276 def UNIXMbox.fromaddr(port)
277 h = HeaderField.new_from_port(port, 'Return-Path') ||
278 HeaderField.new_from_port(port, 'From') ||
279 HeaderField.new_from_port(port, 'EnvelopeSender') or return 'nobody'
280 a = h.addrs[0] or return 'nobody'
281 a.spec
282 end
283
284 def close
285 return if @closed
286
287 ObjectSpace.undefine_finalizer self
288 @finalizer.call
289 @finalizer = nil
290 @real = nil
291 @closed = true
292 @updated = nil
293 end
294
295 def each_port( &block )
296 close_check
297 update
298 @real.each_port(&block)
299 end
300
301 alias each each_port
302
303 def reverse_each_port( &block )
304 close_check
305 update
306 @real.reverse_each_port(&block)
307 end
308
309 alias reverse_each reverse_each_port
310
311 # old #each_mail returns Port
312 #def each_mail( &block )
313 # each_port do |port|
314 # yield Mail.new(port)
315 # end
316 #end
317
318 def each_new_port( mtime = nil )
319 close_check
320 update
321 @real.each_new_port(mtime) {|p| yield p }
322 end
323
324 def new_port
325 close_check
326 @real.new_port
327 end
328
329 private
330
331 def close_check
332 @closed and raise ArgumentError, 'accessing already closed mbox'
333 end
334
335 def update
336 return if FileTest.zero?(@filename)
337 return if @updated and File.mtime(@filename) < @updated
338 w = nil
339 port = nil
340 time = nil
341 UNIXMbox.lock(@filename) {|f|
342 begin
343 f.each do |line|
344 if /\AFrom / === line
345 w.close if w
346 File.utime time, time, port.filename if time
347
348 port = @real.new_port
349 w = port.wopen
350 time = fromline2time(line)
351 else
352 w.print line if w
353 end
354 end
355 ensure
356 if w and not w.closed?
357 w.close
358 File.utime time, time, port.filename if time
359 end
360 end
361 f.truncate(0) unless @readonly
362 @updated = Time.now
363 }
364 end
365
366 def fromline2time( line )
367 m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) or return nil
368 Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
369 rescue
370 nil
371 end
372
373 end # UNIXMbox
374
375 MboxLoader = UNIXMbox
376
377
378 class Maildir
379
380 extend Mutex_m
381
382 PORT_CLASS = MaildirPort
383
384 @seq = 0
385 def Maildir.unique_number
386 synchronize {
387 @seq += 1
388 return @seq
389 }
390 end
391
392 def initialize( dir = nil )
393 @dirname = dir || ENV['MAILDIR']
394 raise ArgumentError, "not directory: #{@dirname}"\
395 unless FileTest.directory? @dirname
396 @new = "#{@dirname}/new"
397 @tmp = "#{@dirname}/tmp"
398 @cur = "#{@dirname}/cur"
399 end
400
401 def directory
402 @dirname
403 end
404
405 def inspect
406 "#<#{self.class} #{@dirname}>"
407 end
408
409 def close
410 end
411
412 def each_port
413 mail_files(@cur).each do |path|
414 yield PORT_CLASS.new(path)
415 end
416 end
417
418 alias each each_port
419
420 def reverse_each_port
421 mail_files(@cur).reverse_each do |path|
422 yield PORT_CLASS.new(path)
423 end
424 end
425
426 alias reverse_each reverse_each_port
427
428 def new_port
429 fname = nil
430 tmpfname = nil
431 newfname = nil
432
433 begin
434 fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
435
436 tmpfname = "#{@tmp}/#{fname}"
437 newfname = "#{@new}/#{fname}"
438 end while FileTest.exist? tmpfname
439
440 if block_given?
441 File.open(tmpfname, 'w') {|f| yield f }
442 File.rename tmpfname, newfname
443 PORT_CLASS.new(newfname)
444 else
445 File.open(tmpfname, 'w') {|f| f.write "\n\n" }
446 PORT_CLASS.new(tmpfname)
447 end
448 end
449
450 def each_new_port
451 mail_files(@new).each do |path|
452 dest = @cur + '/' + File.basename(path)
453 File.rename path, dest
454 yield PORT_CLASS.new(dest)
455 end
456
457 check_tmp
458 end
459
460 TOO_OLD = 60 * 60 * 36 # 36 hour
461
462 def check_tmp
463 old = Time.now.to_i - TOO_OLD
464
465 each_filename(@tmp) do |full, fname|
466 if FileTest.file? full and
467 File.stat(full).mtime.to_i < old
468 File.unlink full
469 end
470 end
471 end
472
473 private
474
475 def mail_files( dir )
476 Dir.entries(dir)\
477 .select {|s| s[0] != ?. }\
478 .sort_by {|s| s.slice(/\A\d+/).to_i }\
479 .map {|s| "#{dir}/#{s}" }\
480 .select {|path| FileTest.file? path }
481 end
482
483 def each_filename( dir )
484 Dir.foreach(dir) do |fname|
485 path = "#{dir}/#{fname}"
486 if fname[0] != ?. and FileTest.file? path
487 yield path, fname
488 end
489 end
490 end
491
492 end # Maildir
493
494 MaildirLoader = Maildir
495
496 end # module TMail
@@ -0,0 +1,6
1 #:stopdoc:
2 require 'tmail/version'
3 require 'tmail/mail'
4 require 'tmail/mailbox'
5 require 'tmail/core_extensions'
6 #:startdoc: No newline at end of file
@@ -0,0 +1,3
1 #:stopdoc:
2 require 'tmail/mailbox'
3 #:startdoc: No newline at end of file
@@ -0,0 +1,250
1 #--
2 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3 #
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 #
23 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24 # with permission of Minero Aoki.
25 #++
26
27 #:stopdoc:
28 require 'nkf'
29 #:startdoc:
30
31 module TMail
32
33 class Mail
34
35 def send_to( smtp )
36 do_send_to(smtp) do
37 ready_to_send
38 end
39 end
40
41 def send_text_to( smtp )
42 do_send_to(smtp) do
43 ready_to_send
44 mime_encode
45 end
46 end
47
48 def do_send_to( smtp )
49 from = from_address or raise ArgumentError, 'no from address'
50 (dests = destinations).empty? and raise ArgumentError, 'no receipient'
51 yield
52 send_to_0 smtp, from, dests
53 end
54 private :do_send_to
55
56 def send_to_0( smtp, from, to )
57 smtp.ready(from, to) do |f|
58 encoded "\r\n", 'j', f, ''
59 end
60 end
61
62 def ready_to_send
63 delete_no_send_fields
64 add_message_id
65 add_date
66 end
67
68 NOSEND_FIELDS = %w(
69 received
70 bcc
71 )
72
73 def delete_no_send_fields
74 NOSEND_FIELDS.each do |nm|
75 delete nm
76 end
77 delete_if {|n,v| v.empty? }
78 end
79
80 def add_message_id( fqdn = nil )
81 unless @message_id_enforced
82 self.message_id = ::TMail::new_message_id(fqdn)
83 end
84 end
85
86 def add_date
87 self.date = Time.now
88 end
89
90 def mime_encode
91 if parts.empty?
92 mime_encode_singlepart
93 else
94 mime_encode_multipart true
95 end
96 end
97
98 def mime_encode_singlepart
99 self.mime_version = '1.0'
100 b = body
101 if NKF.guess(b) != NKF::BINARY
102 mime_encode_text b
103 else
104 mime_encode_binary b
105 end
106 end
107
108 def mime_encode_text( body )
109 self.body = NKF.nkf('-j -m0', body)
110 self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
111 self.encoding = '7bit'
112 end
113
114 def mime_encode_binary( body )
115 self.body = [body].pack('m')
116 self.set_content_type 'application', 'octet-stream'
117 self.encoding = 'Base64'
118 end
119
120 def mime_encode_multipart( top = true )
121 self.mime_version = '1.0' if top
122 self.set_content_type 'multipart', 'mixed'
123 e = encoding(nil)
124 if e and not /\A(?:7bit|8bit|binary)\z/i === e
125 raise ArgumentError,
126 'using C.T.Encoding with multipart mail is not permitted'
127 end
128 end
129
130 end
131
132 #:stopdoc:
133 class DeleteFields
134
135 NOSEND_FIELDS = %w(
136 received
137 bcc
138 )
139
140 def initialize( nosend = nil, delempty = true )
141 @no_send_fields = nosend || NOSEND_FIELDS.dup
142 @delete_empty_fields = delempty
143 end
144
145 attr :no_send_fields
146 attr :delete_empty_fields, true
147
148 def exec( mail )
149 @no_send_fields.each do |nm|
150 delete nm
151 end
152 delete_if {|n,v| v.empty? } if @delete_empty_fields
153 end
154
155 end
156 #:startdoc:
157
158 #:stopdoc:
159 class AddMessageId
160
161 def initialize( fqdn = nil )
162 @fqdn = fqdn
163 end
164
165 attr :fqdn, true
166
167 def exec( mail )
168 mail.message_id = ::TMail::new_msgid(@fqdn)
169 end
170
171 end
172 #:startdoc:
173
174 #:stopdoc:
175 class AddDate
176
177 def exec( mail )
178 mail.date = Time.now
179 end
180
181 end
182 #:startdoc:
183
184 #:stopdoc:
185 class MimeEncodeAuto
186
187 def initialize( s = nil, m = nil )
188 @singlepart_composer = s || MimeEncodeSingle.new
189 @multipart_composer = m || MimeEncodeMulti.new
190 end
191
192 attr :singlepart_composer
193 attr :multipart_composer
194
195 def exec( mail )
196 if mail._builtin_multipart?
197 then @multipart_composer
198 else @singlepart_composer end.exec mail
199 end
200
201 end
202 #:startdoc:
203
204 #:stopdoc:
205 class MimeEncodeSingle
206
207 def exec( mail )
208 mail.mime_version = '1.0'
209 b = mail.body
210 if NKF.guess(b) != NKF::BINARY
211 on_text b
212 else
213 on_binary b
214 end
215 end
216
217 def on_text( body )
218 mail.body = NKF.nkf('-j -m0', body)
219 mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
220 mail.encoding = '7bit'
221 end
222
223 def on_binary( body )
224 mail.body = [body].pack('m')
225 mail.set_content_type 'application', 'octet-stream'
226 mail.encoding = 'Base64'
227 end
228
229 end
230 #:startdoc:
231
232 #:stopdoc:
233 class MimeEncodeMulti
234
235 def exec( mail, top = true )
236 mail.mime_version = '1.0' if top
237 mail.set_content_type 'multipart', 'mixed'
238 e = encoding(nil)
239 if e and not /\A(?:7bit|8bit|binary)\z/i === e
240 raise ArgumentError,
241 'using C.T.Encoding with multipart mail is not permitted'
242 end
243 mail.parts.each do |m|
244 exec m, false if m._builtin_multipart?
245 end
246 end
247
248 end
249 #:startdoc:
250 end # module TMail
@@ -0,0 +1,132
1 =begin rdoc
2
3 = Obsolete methods that are depriciated
4
5 If you really want to see them, go to lib/tmail/obsolete.rb and view to your
6 heart's content.
7
8 =end
9 #--
10 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
11 #
12 # Permission is hereby granted, free of charge, to any person obtaining
13 # a copy of this software and associated documentation files (the
14 # "Software"), to deal in the Software without restriction, including
15 # without limitation the rights to use, copy, modify, merge, publish,
16 # distribute, sublicense, and/or sell copies of the Software, and to
17 # permit persons to whom the Software is furnished to do so, subject to
18 # the following conditions:
19 #
20 # The above copyright notice and this permission notice shall be
21 # included in all copies or substantial portions of the Software.
22 #
23 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 #
31 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
32 # with permission of Minero Aoki.
33 #++
34 #:stopdoc:
35 module TMail #:nodoc:
36
37 class Mail
38 alias include? key?
39 alias has_key? key?
40
41 def values
42 ret = []
43 each_field {|v| ret.push v }
44 ret
45 end
46
47 def value?( val )
48 HeaderField === val or return false
49
50 [ @header[val.name.downcase] ].flatten.include? val
51 end
52
53 alias has_value? value?
54 end
55
56 class Mail
57 def from_addr( default = nil )
58 addr, = from_addrs(nil)
59 addr || default
60 end
61
62 def from_address( default = nil )
63 if a = from_addr(nil)
64 a.spec
65 else
66 default
67 end
68 end
69
70 alias from_address= from_addrs=
71
72 def from_phrase( default = nil )
73 if a = from_addr(nil)
74 a.phrase
75 else
76 default
77 end
78 end
79
80 alias msgid message_id
81 alias msgid= message_id=
82
83 alias each_dest each_destination
84 end
85
86 class Address
87 alias route routes
88 alias addr spec
89
90 def spec=( str )
91 @local, @domain = str.split(/@/,2).map {|s| s.split(/\./) }
92 end
93
94 alias addr= spec=
95 alias address= spec=
96 end
97
98 class MhMailbox
99 alias new_mail new_port
100 alias each_mail each_port
101 alias each_newmail each_new_port
102 end
103 class UNIXMbox
104 alias new_mail new_port
105 alias each_mail each_port
106 alias each_newmail each_new_port
107 end
108 class Maildir
109 alias new_mail new_port
110 alias each_mail each_port
111 alias each_newmail each_new_port
112 end
113
114 extend TextUtils
115
116 class << self
117 alias msgid? message_id?
118 alias boundary new_boundary
119 alias msgid new_message_id
120 alias new_msgid new_message_id
121 end
122
123 def Mail.boundary
124 ::TMail.new_boundary
125 end
126
127 def Mail.msgid
128 ::TMail.new_message_id
129 end
130
131 end # module TMail
132 #:startdoc: No newline at end of file
This diff has been collapsed as it changes many lines, (1060 lines changed) Show them Hide them
@@ -0,0 +1,1060
1 #
2 # DO NOT MODIFY!!!!
3 # This file is automatically generated by racc 1.4.5
4 # from racc grammer file "lib/tmail/parser.y".
5 #
6
7 require 'racc/parser'
8
9
10 #
11 # parser.rb
12 #
13 # Copyright (c) 1998-2007 Minero Aoki
14 #
15 # This program is free software.
16 # You can distribute/modify this program under the terms of
17 # the GNU Lesser General Public License version 2.1.
18 #
19
20 require 'tmail/scanner'
21 require 'tmail/utils'
22
23
24 module TMail
25
26 class Parser < Racc::Parser
27
28 module_eval <<'..end lib/tmail/parser.y modeval..id2dd1c7d21d', 'lib/tmail/parser.y', 340
29
30 include TextUtils
31
32 def self.parse( ident, str, cmt = nil )
33 str = special_quote_address(str) if ident.to_s =~ /M?ADDRESS/
34 new.parse(ident, str, cmt)
35 end
36
37 def self.special_quote_address(str) #:nodoc:
38 # Takes a string which is an address and adds quotation marks to special
39 # edge case methods that the RACC parser can not handle.
40 #
41 # Right now just handles two edge cases:
42 #
43 # Full stop as the last character of the display name:
44 # Mikel L. <mikel@me.com>
45 # Returns:
46 # "Mikel L." <mikel@me.com>
47 #
48 # Unquoted @ symbol in the display name:
49 # mikel@me.com <mikel@me.com>
50 # Returns:
51 # "mikel@me.com" <mikel@me.com>
52 #
53 # Any other address not matching these patterns just gets returned as is.
54 case
55 # This handles the missing "" in an older version of Apple Mail.app
56 # around the display name when the display name contains a '@'
57 # like 'mikel@me.com <mikel@me.com>'
58 # Just quotes it to: '"mikel@me.com" <mikel@me.com>'
59 when str =~ /\A([^"].+@.+[^"])\s(<.*?>)\Z/
60 return "\"#{$1}\" #{$2}"
61 # This handles cases where 'Mikel A. <mikel@me.com>' which is a trailing
62 # full stop before the address section. Just quotes it to
63 # '"Mikel A." <mikel@me.com>'
64 when str =~ /\A(.*?\.)\s(<.*?>)\s*\Z/
65 return "\"#{$1}\" #{$2}"
66 else
67 str
68 end
69 end
70
71 MAILP_DEBUG = false
72
73 def initialize
74 self.debug = MAILP_DEBUG
75 end
76
77 def debug=( flag )
78 @yydebug = flag && Racc_debug_parser
79 @scanner_debug = flag
80 end
81
82 def debug
83 @yydebug
84 end
85
86 def parse( ident, str, comments = nil )
87 @scanner = Scanner.new(str, ident, comments)
88 @scanner.debug = @scanner_debug
89 @first = [ident, ident]
90 result = yyparse(self, :parse_in)
91 comments.map! {|c| to_kcode(c) } if comments
92 result
93 end
94
95 private
96
97 def parse_in( &block )
98 yield @first
99 @scanner.scan(&block)
100 end
101
102 def on_error( t, val, vstack )
103 raise TMail::SyntaxError, "parse error on token #{racc_token2str t}"
104 end
105
106 ..end lib/tmail/parser.y modeval..id2dd1c7d21d
107
108 ##### racc 1.4.5 generates ###
109
110 racc_reduce_table = [
111 0, 0, :racc_error,
112 2, 35, :_reduce_1,
113 2, 35, :_reduce_2,
114 2, 35, :_reduce_3,
115 2, 35, :_reduce_4,
116 2, 35, :_reduce_5,
117 2, 35, :_reduce_6,
118 2, 35, :_reduce_7,
119 2, 35, :_reduce_8,
120 2, 35, :_reduce_9,
121 2, 35, :_reduce_10,
122 2, 35, :_reduce_11,
123 2, 35, :_reduce_12,
124 6, 36, :_reduce_13,
125 0, 48, :_reduce_none,
126 2, 48, :_reduce_none,
127 3, 49, :_reduce_16,
128 5, 49, :_reduce_17,
129 1, 50, :_reduce_18,
130 7, 37, :_reduce_19,
131 0, 51, :_reduce_none,
132 2, 51, :_reduce_21,
133 0, 52, :_reduce_none,
134 2, 52, :_reduce_23,
135 1, 58, :_reduce_24,
136 3, 58, :_reduce_25,
137 2, 58, :_reduce_26,
138 0, 53, :_reduce_none,
139 2, 53, :_reduce_28,
140 0, 54, :_reduce_29,
141 3, 54, :_reduce_30,
142 0, 55, :_reduce_none,
143 2, 55, :_reduce_32,
144 2, 55, :_reduce_33,
145 0, 56, :_reduce_none,
146 2, 56, :_reduce_35,
147 1, 61, :_reduce_36,
148 1, 61, :_reduce_37,
149 0, 57, :_reduce_none,
150 2, 57, :_reduce_39,
151 1, 38, :_reduce_none,
152 1, 38, :_reduce_none,
153 3, 38, :_reduce_none,
154 1, 46, :_reduce_none,
155 1, 46, :_reduce_none,
156 1, 46, :_reduce_none,
157 1, 39, :_reduce_none,
158 2, 39, :_reduce_47,
159 1, 64, :_reduce_48,
160 3, 64, :_reduce_49,
161 1, 68, :_reduce_none,
162 1, 68, :_reduce_none,
163 1, 69, :_reduce_52,
164 3, 69, :_reduce_53,
165 1, 47, :_reduce_none,
166 1, 47, :_reduce_none,
167 2, 47, :_reduce_56,
168 2, 67, :_reduce_none,
169 3, 65, :_reduce_58,
170 2, 65, :_reduce_59,
171 1, 70, :_reduce_60,
172 2, 70, :_reduce_61,
173 4, 62, :_reduce_62,
174 3, 62, :_reduce_63,
175 2, 72, :_reduce_none,
176 2, 73, :_reduce_65,
177 4, 73, :_reduce_66,
178 3, 63, :_reduce_67,
179 1, 63, :_reduce_68,
180 1, 74, :_reduce_none,
181 2, 74, :_reduce_70,
182 1, 71, :_reduce_71,
183 3, 71, :_reduce_72,
184 1, 59, :_reduce_73,
185 3, 59, :_reduce_74,
186 1, 76, :_reduce_75,
187 2, 76, :_reduce_76,
188 1, 75, :_reduce_none,
189 1, 75, :_reduce_none,
190 1, 75, :_reduce_none,
191 1, 77, :_reduce_none,
192 1, 77, :_reduce_none,
193 1, 77, :_reduce_none,
194 1, 66, :_reduce_none,
195 2, 66, :_reduce_none,
196 3, 60, :_reduce_85,
197 1, 40, :_reduce_86,
198 3, 40, :_reduce_87,
199 1, 79, :_reduce_none,
200 2, 79, :_reduce_89,
201 1, 41, :_reduce_90,
202 2, 41, :_reduce_91,
203 3, 42, :_reduce_92,
204 5, 43, :_reduce_93,
205 3, 43, :_reduce_94,
206 0, 80, :_reduce_95,
207 5, 80, :_reduce_96,
208 5, 80, :_reduce_97,
209 1, 44, :_reduce_98,
210 3, 45, :_reduce_99,
211 0, 81, :_reduce_none,
212 1, 81, :_reduce_none,
213 1, 78, :_reduce_none,
214 1, 78, :_reduce_none,
215 1, 78, :_reduce_none,
216 1, 78, :_reduce_none,
217 1, 78, :_reduce_none,
218 1, 78, :_reduce_none,
219 1, 78, :_reduce_none ]
220
221 racc_reduce_n = 109
222
223 racc_shift_n = 167
224
225 racc_action_table = [
226 -69, 130, -70, 23, 25, 153, 94, 29, 31, 142,
227 143, 16, 17, 20, 22, 98, -69, 154, -70, 32,
228 -69, 107, -70, 145, 146, 78, -69, 91, -70, 75,
229 -70, 23, 25, 120, 88, 29, 31, 105, 106, 16,
230 17, 20, 22, 81, 27, 23, 25, 32, 112, 29,
231 31, 96, 80, 16, 17, 20, 22, 117, 27, 23,
232 25, 32, 79, 29, 31, 78, 123, 16, 17, 20,
233 22, 100, 27, 23, 25, 32, 125, 29, 31, 113,
234 115, 16, 17, 20, 22, 126, 23, 25, 101, 32,
235 29, 31, 91, 128, 16, 17, 20, 22, 129, 27,
236 23, 25, 32, 101, 29, 31, 101, 75, 16, 17,
237 20, 22, 77, 52, 23, 25, 32, 65, 29, 31,
238 133, 78, 16, 17, 20, 22, 62, 23, 25, 136,
239 32, 29, 31, 60, 44, 16, 17, 20, 22, 139,
240 23, 25, 101, 32, 29, 31, 101, 100, 16, 17,
241 20, 22, 100, 27, 23, 25, 32, 101, 29, 31,
242 147, 148, 16, 17, 20, 22, 151, 23, 25, 152,
243 32, 29, 31, 74, 42, 16, 17, 20, 22, 156,
244 158, 92, 40, 32, 23, 25, 15, 68, 29, 31,
245 163, 40, 16, 17, 20, 22, 165, 27, 23, 25,
246 32, 166, 29, 31, nil, nil, 16, 17, 20, 22,
247 nil, 27, 23, 25, 32, nil, 29, 31, nil, nil,
248 16, 17, 20, 22, nil, 23, 25, nil, 32, 29,
249 31, nil, nil, 16, 17, 20, 22, nil, 23, 25,
250 nil, 32, 29, 31, nil, nil, 16, 17, 20, 22,
251 nil, 23, 25, nil, 32, 29, 31, nil, nil, 16,
252 17, 20, 22, nil, 27, nil, nil, 32, 23, 25,
253 120, nil, 29, 31, nil, nil, 16, 17, 20, 22,
254 nil, 27, 23, 25, 32, nil, 29, 31, nil, nil,
255 16, 17, 20, 22, nil, 23, 25, 109, 32, 29,
256 31, 74, nil, 16, 17, 20, 22, nil, 84, 25,
257 nil, 32, 29, 31, nil, 87, 16, 17, 20, 22,
258 84, 25, nil, 109, 29, 31, nil, 87, 16, 17,
259 20, 22, 84, 25, nil, nil, 29, 31, nil, 87,
260 16, 17, 20, 22, 84, 25, nil, nil, 29, 31,
261 nil, 87, 16, 17, 20, 22, 84, 25, nil, nil,
262 29, 31, nil, 87, 16, 17, 20, 22, 84, 25,
263 nil, nil, 29, 31, nil, 87, 16, 17, 20, 22,
264 4, 6, 7, 8, 9, 10, 11, 12, 13, 1,
265 2, 3, 84, 25, nil, nil, 29, 31, nil, 87,
266 16, 17, 20, 22 ]
267
268 racc_action_check = [
269 28, 112, 75, 71, 71, 143, 56, 71, 71, 134,
270 134, 71, 71, 71, 71, 62, 28, 143, 75, 71,
271 28, 73, 75, 136, 136, 51, 28, 50, 75, 28,
272 75, 127, 127, 127, 45, 127, 127, 72, 72, 127,
273 127, 127, 127, 42, 127, 3, 3, 127, 80, 3,
274 3, 60, 41, 3, 3, 3, 3, 89, 3, 151,
275 151, 3, 40, 151, 151, 36, 96, 151, 151, 151,
276 151, 97, 151, 55, 55, 151, 98, 55, 55, 86,
277 86, 55, 55, 55, 55, 100, 7, 7, 86, 55,
278 7, 7, 102, 104, 7, 7, 7, 7, 105, 7,
279 8, 8, 7, 108, 8, 8, 111, 70, 8, 8,
280 8, 8, 33, 8, 9, 9, 8, 13, 9, 9,
281 117, 121, 9, 9, 9, 9, 12, 10, 10, 126,
282 9, 10, 10, 11, 6, 10, 10, 10, 10, 130,
283 2, 2, 131, 10, 2, 2, 67, 135, 2, 2,
284 2, 2, 66, 2, 122, 122, 2, 138, 122, 122,
285 139, 140, 122, 122, 122, 122, 141, 52, 52, 142,
286 122, 52, 52, 52, 5, 52, 52, 52, 52, 147,
287 150, 52, 4, 52, 26, 26, 1, 26, 26, 26,
288 156, 158, 26, 26, 26, 26, 162, 26, 68, 68,
289 26, 163, 68, 68, nil, nil, 68, 68, 68, 68,
290 nil, 68, 59, 59, 68, nil, 59, 59, nil, nil,
291 59, 59, 59, 59, nil, 154, 154, nil, 59, 154,
292 154, nil, nil, 154, 154, 154, 154, nil, 94, 94,
293 nil, 154, 94, 94, nil, nil, 94, 94, 94, 94,
294 nil, 38, 38, nil, 94, 38, 38, nil, nil, 38,
295 38, 38, 38, nil, 38, nil, nil, 38, 90, 90,
296 90, nil, 90, 90, nil, nil, 90, 90, 90, 90,
297 nil, 90, 76, 76, 90, nil, 76, 76, nil, nil,
298 76, 76, 76, 76, nil, 27, 27, 76, 76, 27,
299 27, 27, nil, 27, 27, 27, 27, nil, 114, 114,
300 nil, 27, 114, 114, nil, 114, 114, 114, 114, 114,
301 44, 44, nil, 114, 44, 44, nil, 44, 44, 44,
302 44, 44, 74, 74, nil, nil, 74, 74, nil, 74,
303 74, 74, 74, 74, 113, 113, nil, nil, 113, 113,
304 nil, 113, 113, 113, 113, 113, 129, 129, nil, nil,
305 129, 129, nil, 129, 129, 129, 129, 129, 88, 88,
306 nil, nil, 88, 88, nil, 88, 88, 88, 88, 88,
307 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
308 0, 0, 77, 77, nil, nil, 77, 77, nil, 77,
309 77, 77, 77, 77 ]
310
311 racc_action_pointer = [
312 378, 155, 126, 31, 167, 174, 116, 72, 86, 100,
313 113, 119, 95, 86, nil, nil, nil, nil, nil, nil,
314 nil, nil, nil, nil, nil, nil, 170, 281, 0, nil,
315 nil, nil, nil, 92, nil, nil, 39, nil, 237, nil,
316 46, 38, 43, nil, 306, 15, nil, nil, nil, nil,
317 11, -1, 153, nil, nil, 59, -10, nil, nil, 198,
318 22, nil, -17, nil, nil, nil, 126, 117, 184, nil,
319 78, -11, 21, -7, 318, 2, 268, 378, nil, nil,
320 33, nil, nil, nil, nil, nil, 59, nil, 354, 35,
321 254, nil, nil, nil, 224, nil, 52, 45, 45, nil,
322 54, nil, 76, nil, 65, 78, nil, nil, 74, nil,
323 nil, 77, -13, 330, 294, nil, nil, 105, nil, nil,
324 nil, 95, 140, nil, nil, nil, 96, 17, nil, 342,
325 125, 113, nil, nil, -14, 121, -7, nil, 128, 143,
326 146, 141, 154, -10, nil, nil, nil, 165, nil, nil,
327 154, 45, nil, nil, 211, nil, 173, nil, 176, nil,
328 nil, nil, 168, 187, nil, nil, nil ]
329
330 racc_action_default = [
331 -109, -109, -109, -109, -14, -109, -20, -109, -109, -109,
332 -109, -109, -109, -109, -10, -95, -105, -106, -77, -44,
333 -107, -11, -108, -79, -43, -102, -109, -109, -60, -103,
334 -55, -104, -78, -68, -54, -71, -45, -12, -109, -1,
335 -109, -109, -109, -2, -109, -22, -51, -48, -50, -3,
336 -40, -41, -109, -46, -4, -86, -5, -88, -6, -90,
337 -109, -7, -95, -8, -9, -98, -100, -61, -59, -56,
338 -69, -109, -109, -109, -109, -75, -109, -109, -57, -15,
339 -109, 167, -73, -80, -82, -21, -24, -81, -109, -27,
340 -109, -83, -47, -89, -109, -91, -109, -100, -109, -99,
341 -101, -75, -58, -52, -109, -109, -64, -63, -65, -76,
342 -72, -67, -109, -109, -109, -26, -23, -109, -29, -49,
343 -84, -42, -87, -92, -94, -95, -109, -109, -62, -109,
344 -109, -25, -74, -28, -31, -100, -109, -53, -66, -109,
345 -109, -34, -109, -109, -93, -96, -97, -109, -18, -13,
346 -38, -109, -30, -33, -109, -32, -16, -19, -14, -35,
347 -36, -37, -109, -109, -39, -85, -17 ]
348
349 racc_goto_table = [
350 39, 67, 70, 73, 38, 66, 69, 24, 37, 57,
351 59, 36, 55, 67, 99, 90, 85, 157, 69, 108,
352 83, 134, 111, 76, 49, 53, 141, 70, 73, 150,
353 118, 89, 45, 155, 159, 149, 140, 21, 14, 19,
354 119, 102, 64, 63, 61, 124, 70, 104, 58, 132,
355 83, 56, 97, 83, 54, 93, 43, 5, 131, 95,
356 116, nil, 76, nil, 83, 76, nil, 127, nil, 38,
357 nil, nil, nil, 103, 138, nil, 110, nil, nil, nil,
358 nil, nil, nil, 144, nil, nil, nil, nil, nil, 83,
359 83, nil, nil, nil, 57, nil, nil, 122, nil, 121,
360 nil, nil, nil, nil, nil, 83, nil, nil, nil, nil,
361 nil, nil, nil, nil, nil, 135, nil, nil, nil, nil,
362 nil, nil, 93, nil, nil, nil, 70, 161, 38, 70,
363 162, 160, 137, nil, nil, nil, nil, nil, nil, nil,
364 nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
365 nil, nil, nil, nil, 164 ]
366
367 racc_goto_check = [
368 2, 37, 37, 29, 36, 46, 28, 13, 13, 41,
369 41, 31, 45, 37, 47, 32, 24, 23, 28, 25,
370 44, 20, 25, 42, 4, 4, 21, 37, 29, 22,
371 19, 18, 17, 26, 27, 16, 15, 12, 11, 33,
372 34, 35, 10, 9, 8, 47, 37, 29, 7, 43,
373 44, 6, 46, 44, 5, 41, 3, 1, 25, 41,
374 24, nil, 42, nil, 44, 42, nil, 32, nil, 36,
375 nil, nil, nil, 13, 25, nil, 41, nil, nil, nil,
376 nil, nil, nil, 47, nil, nil, nil, nil, nil, 44,
377 44, nil, nil, nil, 41, nil, nil, 45, nil, 31,
378 nil, nil, nil, nil, nil, 44, nil, nil, nil, nil,
379 nil, nil, nil, nil, nil, 46, nil, nil, nil, nil,
380 nil, nil, 41, nil, nil, nil, 37, 29, 36, 37,
381 29, 28, 13, nil, nil, nil, nil, nil, nil, nil,
382 nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
383 nil, nil, nil, nil, 2 ]
384
385 racc_goto_pointer = [
386 nil, 57, -4, 50, 17, 46, 42, 38, 33, 31,
387 29, 37, 35, 5, nil, -94, -105, 26, -14, -59,
388 -97, -108, -112, -133, -28, -55, -110, -117, -20, -24,
389 nil, 9, -35, 37, -50, -27, 1, -25, nil, nil,
390 nil, 0, -5, -65, -24, 3, -10, -52 ]
391
392 racc_goto_default = [
393 nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
394 nil, nil, nil, 48, 41, nil, nil, nil, nil, nil,
395 nil, nil, nil, nil, nil, 86, nil, nil, 30, 34,
396 50, 51, nil, 46, 47, nil, 26, 28, 71, 72,
397 33, 35, 114, 82, 18, nil, nil, nil ]
398
399 racc_token_table = {
400 false => 0,
401 Object.new => 1,
402 :DATETIME => 2,
403 :RECEIVED => 3,
404 :MADDRESS => 4,
405 :RETPATH => 5,
406 :KEYWORDS => 6,
407 :ENCRYPTED => 7,
408 :MIMEVERSION => 8,
409 :CTYPE => 9,
410 :CENCODING => 10,
411 :CDISPOSITION => 11,
412 :ADDRESS => 12,
413 :MAILBOX => 13,
414 :DIGIT => 14,
415 :ATOM => 15,
416 "," => 16,
417 ":" => 17,
418 :FROM => 18,
419 :BY => 19,
420 "@" => 20,
421 :DOMLIT => 21,
422 :VIA => 22,
423 :WITH => 23,
424 :ID => 24,
425 :FOR => 25,
426 ";" => 26,
427 "<" => 27,
428 ">" => 28,
429 "." => 29,
430 :QUOTED => 30,
431 :TOKEN => 31,
432 "/" => 32,
433 "=" => 33 }
434
435 racc_use_result_var = false
436
437 racc_nt_base = 34
438
439 Racc_arg = [
440 racc_action_table,
441 racc_action_check,
442 racc_action_default,
443 racc_action_pointer,
444 racc_goto_table,
445 racc_goto_check,
446 racc_goto_default,
447 racc_goto_pointer,
448 racc_nt_base,
449 racc_reduce_table,
450 racc_token_table,
451 racc_shift_n,
452 racc_reduce_n,
453 racc_use_result_var ]
454
455 Racc_token_to_s_table = [
456 '$end',
457 'error',
458 'DATETIME',
459 'RECEIVED',
460 'MADDRESS',
461 'RETPATH',
462 'KEYWORDS',
463 'ENCRYPTED',
464 'MIMEVERSION',
465 'CTYPE',
466 'CENCODING',
467 'CDISPOSITION',
468 'ADDRESS',
469 'MAILBOX',
470 'DIGIT',
471 'ATOM',
472 '","',
473 '":"',
474 'FROM',
475 'BY',
476 '"@"',
477 'DOMLIT',
478 'VIA',
479 'WITH',
480 'ID',
481 'FOR',
482 '";"',
483 '"<"',
484 '">"',
485 '"."',
486 'QUOTED',
487 'TOKEN',
488 '"/"',
489 '"="',
490 '$start',
491 'content',
492 'datetime',
493 'received',
494 'addrs_TOP',
495 'retpath',
496 'keys',
497 'enc',
498 'version',
499 'ctype',
500 'cencode',
501 'cdisp',
502 'addr_TOP',
503 'mbox',
504 'day',
505 'hour',
506 'zone',
507 'from',
508 'by',
509 'via',
510 'with',
511 'id',
512 'for',
513 'received_datetime',
514 'received_domain',
515 'domain',
516 'msgid',
517 'received_addrspec',
518 'routeaddr',
519 'spec',
520 'addrs',
521 'group_bare',
522 'commas',
523 'group',
524 'addr',
525 'mboxes',
526 'addr_phrase',
527 'local_head',
528 'routes',
529 'at_domains',
530 'local',
531 'word',
532 'dots',
533 'domword',
534 'atom',
535 'phrase',
536 'params',
537 'opt_semicolon']
538
539 Racc_debug_parser = false
540
541 ##### racc system variables end #####
542
543 # reduce 0 omitted
544
545 module_eval <<'.,.,', 'lib/tmail/parser.y', 16
546 def _reduce_1( val, _values)
547 val[1]
548 end
549 .,.,
550
551 module_eval <<'.,.,', 'lib/tmail/parser.y', 17
552 def _reduce_2( val, _values)
553 val[1]
554 end
555 .,.,
556
557 module_eval <<'.,.,', 'lib/tmail/parser.y', 18
558 def _reduce_3( val, _values)
559 val[1]
560 end
561 .,.,
562
563 module_eval <<'.,.,', 'lib/tmail/parser.y', 19
564 def _reduce_4( val, _values)
565 val[1]
566 end
567 .,.,
568
569 module_eval <<'.,.,', 'lib/tmail/parser.y', 20
570 def _reduce_5( val, _values)
571 val[1]
572 end
573 .,.,
574
575 module_eval <<'.,.,', 'lib/tmail/parser.y', 21
576 def _reduce_6( val, _values)
577 val[1]
578 end
579 .,.,
580
581 module_eval <<'.,.,', 'lib/tmail/parser.y', 22
582 def _reduce_7( val, _values)
583 val[1]
584 end
585 .,.,
586
587 module_eval <<'.,.,', 'lib/tmail/parser.y', 23
588 def _reduce_8( val, _values)
589 val[1]
590 end
591 .,.,
592
593 module_eval <<'.,.,', 'lib/tmail/parser.y', 24
594 def _reduce_9( val, _values)
595 val[1]
596 end
597 .,.,
598
599 module_eval <<'.,.,', 'lib/tmail/parser.y', 25
600 def _reduce_10( val, _values)
601 val[1]
602 end
603 .,.,
604
605 module_eval <<'.,.,', 'lib/tmail/parser.y', 26
606 def _reduce_11( val, _values)
607 val[1]
608 end
609 .,.,
610
611 module_eval <<'.,.,', 'lib/tmail/parser.y', 27
612 def _reduce_12( val, _values)
613 val[1]
614 end
615 .,.,
616
617 module_eval <<'.,.,', 'lib/tmail/parser.y', 36
618 def _reduce_13( val, _values)
619 t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
620 (t + val[4] - val[5]).localtime
621 end
622 .,.,
623
624 # reduce 14 omitted
625
626 # reduce 15 omitted
627
628 module_eval <<'.,.,', 'lib/tmail/parser.y', 45
629 def _reduce_16( val, _values)
630 (val[0].to_i * 60 * 60) +
631 (val[2].to_i * 60)
632 end
633 .,.,
634
635 module_eval <<'.,.,', 'lib/tmail/parser.y', 51
636 def _reduce_17( val, _values)
637 (val[0].to_i * 60 * 60) +
638 (val[2].to_i * 60) +
639 (val[4].to_i)
640 end
641 .,.,
642
643 module_eval <<'.,.,', 'lib/tmail/parser.y', 56
644 def _reduce_18( val, _values)
645 timezone_string_to_unixtime(val[0])
646 end
647 .,.,
648
649 module_eval <<'.,.,', 'lib/tmail/parser.y', 61
650 def _reduce_19( val, _values)
651 val
652 end
653 .,.,
654
655 # reduce 20 omitted
656
657 module_eval <<'.,.,', 'lib/tmail/parser.y', 67
658 def _reduce_21( val, _values)
659 val[1]
660 end
661 .,.,
662
663 # reduce 22 omitted
664
665 module_eval <<'.,.,', 'lib/tmail/parser.y', 73
666 def _reduce_23( val, _values)
667 val[1]
668 end
669 .,.,
670
671 module_eval <<'.,.,', 'lib/tmail/parser.y', 79
672 def _reduce_24( val, _values)
673 join_domain(val[0])
674 end
675 .,.,
676
677 module_eval <<'.,.,', 'lib/tmail/parser.y', 83
678 def _reduce_25( val, _values)
679 join_domain(val[2])
680 end
681 .,.,
682
683 module_eval <<'.,.,', 'lib/tmail/parser.y', 87
684 def _reduce_26( val, _values)
685 join_domain(val[0])
686 end
687 .,.,
688
689 # reduce 27 omitted
690
691 module_eval <<'.,.,', 'lib/tmail/parser.y', 93
692 def _reduce_28( val, _values)
693 val[1]
694 end
695 .,.,
696
697 module_eval <<'.,.,', 'lib/tmail/parser.y', 98
698 def _reduce_29( val, _values)
699 []
700 end
701 .,.,
702
703 module_eval <<'.,.,', 'lib/tmail/parser.y', 103
704 def _reduce_30( val, _values)
705 val[0].push val[2]
706 val[0]
707 end
708 .,.,
709
710 # reduce 31 omitted
711
712 module_eval <<'.,.,', 'lib/tmail/parser.y', 109
713 def _reduce_32( val, _values)
714 val[1]
715 end
716 .,.,
717
718 module_eval <<'.,.,', 'lib/tmail/parser.y', 113
719 def _reduce_33( val, _values)
720 val[1]
721 end
722 .,.,
723
724 # reduce 34 omitted
725
726 module_eval <<'.,.,', 'lib/tmail/parser.y', 119
727 def _reduce_35( val, _values)
728 val[1]
729 end
730 .,.,
731
732 module_eval <<'.,.,', 'lib/tmail/parser.y', 125
733 def _reduce_36( val, _values)
734 val[0].spec
735 end
736 .,.,
737
738 module_eval <<'.,.,', 'lib/tmail/parser.y', 129
739 def _reduce_37( val, _values)
740 val[0].spec
741 end
742 .,.,
743
744 # reduce 38 omitted
745
746 module_eval <<'.,.,', 'lib/tmail/parser.y', 136
747 def _reduce_39( val, _values)
748 val[1]
749 end
750 .,.,
751
752 # reduce 40 omitted
753
754 # reduce 41 omitted
755
756 # reduce 42 omitted
757
758 # reduce 43 omitted
759
760 # reduce 44 omitted
761
762 # reduce 45 omitted
763
764 # reduce 46 omitted
765
766 module_eval <<'.,.,', 'lib/tmail/parser.y', 146
767 def _reduce_47( val, _values)
768 [ Address.new(nil, nil) ]
769 end
770 .,.,
771
772 module_eval <<'.,.,', 'lib/tmail/parser.y', 152
773 def _reduce_48( val, _values)
774 val
775 end
776 .,.,
777
778 module_eval <<'.,.,', 'lib/tmail/parser.y', 157
779 def _reduce_49( val, _values)
780 val[0].push val[2]
781 val[0]
782 end
783 .,.,
784
785 # reduce 50 omitted
786
787 # reduce 51 omitted
788
789 module_eval <<'.,.,', 'lib/tmail/parser.y', 165
790 def _reduce_52( val, _values)
791 val
792 end
793 .,.,
794
795 module_eval <<'.,.,', 'lib/tmail/parser.y', 170
796 def _reduce_53( val, _values)
797 val[0].push val[2]
798 val[0]
799 end
800 .,.,
801
802 # reduce 54 omitted
803
804 # reduce 55 omitted
805
806 module_eval <<'.,.,', 'lib/tmail/parser.y', 178
807 def _reduce_56( val, _values)
808 val[1].phrase = Decoder.decode(val[0])
809 val[1]
810 end
811 .,.,
812
813 # reduce 57 omitted
814
815 module_eval <<'.,.,', 'lib/tmail/parser.y', 185
816 def _reduce_58( val, _values)
817 AddressGroup.new(val[0], val[2])
818 end
819 .,.,
820
821 module_eval <<'.,.,', 'lib/tmail/parser.y', 185
822 def _reduce_59( val, _values)
823 AddressGroup.new(val[0], [])
824 end
825 .,.,
826
827 module_eval <<'.,.,', 'lib/tmail/parser.y', 188
828 def _reduce_60( val, _values)
829 val[0].join('.')
830 end
831 .,.,
832
833 module_eval <<'.,.,', 'lib/tmail/parser.y', 189
834 def _reduce_61( val, _values)
835 val[0] << ' ' << val[1].join('.')
836 end
837 .,.,
838
839 module_eval <<'.,.,', 'lib/tmail/parser.y', 196
840 def _reduce_62( val, _values)
841 val[2].routes.replace val[1]
842 val[2]
843 end
844 .,.,
845
846 module_eval <<'.,.,', 'lib/tmail/parser.y', 200
847 def _reduce_63( val, _values)
848 val[1]
849 end
850 .,.,
851
852 # reduce 64 omitted
853
854 module_eval <<'.,.,', 'lib/tmail/parser.y', 203
855 def _reduce_65( val, _values)
856 [ val[1].join('.') ]
857 end
858 .,.,
859
860 module_eval <<'.,.,', 'lib/tmail/parser.y', 204
861 def _reduce_66( val, _values)
862 val[0].push val[3].join('.'); val[0]
863 end
864 .,.,
865
866 module_eval <<'.,.,', 'lib/tmail/parser.y', 206
867 def _reduce_67( val, _values)
868 Address.new( val[0], val[2] )
869 end
870 .,.,
871
872 module_eval <<'.,.,', 'lib/tmail/parser.y', 207
873 def _reduce_68( val, _values)
874 Address.new( val[0], nil )
875 end
876 .,.,
877
878 # reduce 69 omitted
879
880 module_eval <<'.,.,', 'lib/tmail/parser.y', 210
881 def _reduce_70( val, _values)
882 val[0].push ''; val[0]
883 end
884 .,.,
885
886 module_eval <<'.,.,', 'lib/tmail/parser.y', 213
887 def _reduce_71( val, _values)
888 val
889 end
890 .,.,
891
892 module_eval <<'.,.,', 'lib/tmail/parser.y', 222
893 def _reduce_72( val, _values)
894 val[1].times do
895 val[0].push ''
896 end
897 val[0].push val[2]
898 val[0]
899 end
900 .,.,
901
902 module_eval <<'.,.,', 'lib/tmail/parser.y', 224
903 def _reduce_73( val, _values)
904 val
905 end
906 .,.,
907
908 module_eval <<'.,.,', 'lib/tmail/parser.y', 233
909 def _reduce_74( val, _values)
910 val[1].times do
911 val[0].push ''
912 end
913 val[0].push val[2]
914 val[0]
915 end
916 .,.,
917
918 module_eval <<'.,.,', 'lib/tmail/parser.y', 234
919 def _reduce_75( val, _values)
920 0
921 end
922 .,.,
923
924 module_eval <<'.,.,', 'lib/tmail/parser.y', 235
925 def _reduce_76( val, _values)
926 val[0] + 1
927 end
928 .,.,
929
930 # reduce 77 omitted
931
932 # reduce 78 omitted
933
934 # reduce 79 omitted
935
936 # reduce 80 omitted
937
938 # reduce 81 omitted
939
940 # reduce 82 omitted
941
942 # reduce 83 omitted
943
944 # reduce 84 omitted
945
946 module_eval <<'.,.,', 'lib/tmail/parser.y', 253
947 def _reduce_85( val, _values)
948 val[1] = val[1].spec
949 val.join('')
950 end
951 .,.,
952
953 module_eval <<'.,.,', 'lib/tmail/parser.y', 254
954 def _reduce_86( val, _values)
955 val
956 end
957 .,.,
958
959 module_eval <<'.,.,', 'lib/tmail/parser.y', 255
960 def _reduce_87( val, _values)
961 val[0].push val[2]; val[0]
962 end
963 .,.,
964
965 # reduce 88 omitted
966
967 module_eval <<'.,.,', 'lib/tmail/parser.y', 258
968 def _reduce_89( val, _values)
969 val[0] << ' ' << val[1]
970 end
971 .,.,
972
973 module_eval <<'.,.,', 'lib/tmail/parser.y', 265
974 def _reduce_90( val, _values)
975 val.push nil
976 val
977 end
978 .,.,
979
980 module_eval <<'.,.,', 'lib/tmail/parser.y', 269
981 def _reduce_91( val, _values)
982 val
983 end
984 .,.,
985
986 module_eval <<'.,.,', 'lib/tmail/parser.y', 274
987 def _reduce_92( val, _values)
988 [ val[0].to_i, val[2].to_i ]
989 end
990 .,.,
991
992 module_eval <<'.,.,', 'lib/tmail/parser.y', 279
993 def _reduce_93( val, _values)
994 [ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
995 end
996 .,.,
997
998 module_eval <<'.,.,', 'lib/tmail/parser.y', 283
999 def _reduce_94( val, _values)
1000 [ val[0].downcase, nil, decode_params(val[1]) ]
1001 end
1002 .,.,
1003
1004 module_eval <<'.,.,', 'lib/tmail/parser.y', 288
1005 def _reduce_95( val, _values)
1006 {}
1007 end
1008 .,.,
1009
1010 module_eval <<'.,.,', 'lib/tmail/parser.y', 293
1011 def _reduce_96( val, _values)
1012 val[0][ val[2].downcase ] = ('"' + val[4].to_s + '"')
1013 val[0]
1014 end
1015 .,.,
1016
1017 module_eval <<'.,.,', 'lib/tmail/parser.y', 298
1018 def _reduce_97( val, _values)
1019 val[0][ val[2].downcase ] = val[4]
1020 val[0]
1021 end
1022 .,.,
1023
1024 module_eval <<'.,.,', 'lib/tmail/parser.y', 303
1025 def _reduce_98( val, _values)
1026 val[0].downcase
1027 end
1028 .,.,
1029
1030 module_eval <<'.,.,', 'lib/tmail/parser.y', 308
1031 def _reduce_99( val, _values)
1032 [ val[0].downcase, decode_params(val[1]) ]
1033 end
1034 .,.,
1035
1036 # reduce 100 omitted
1037
1038 # reduce 101 omitted
1039
1040 # reduce 102 omitted
1041
1042 # reduce 103 omitted
1043
1044 # reduce 104 omitted
1045
1046 # reduce 105 omitted
1047
1048 # reduce 106 omitted
1049
1050 # reduce 107 omitted
1051
1052 # reduce 108 omitted
1053
1054 def _reduce_none( val, _values)
1055 val[0]
1056 end
1057
1058 end # class Parser
1059
1060 end # module TMail
@@ -0,0 +1,416
1 #
2 # parser.y
3 #
4 # Copyright (c) 1998-2007 Minero Aoki
5 #
6 # This program is free software.
7 # You can distribute/modify this program under the terms of
8 # the GNU Lesser General Public License version 2.1.
9 #
10
11 class TMail::Parser
12
13 options no_result_var
14
15 rule
16
17 content : DATETIME datetime { val[1] }
18 | RECEIVED received { val[1] }
19 | MADDRESS addrs_TOP { val[1] }
20 | RETPATH retpath { val[1] }
21 | KEYWORDS keys { val[1] }
22 | ENCRYPTED enc { val[1] }
23 | MIMEVERSION version { val[1] }
24 | CTYPE ctype { val[1] }
25 | CENCODING cencode { val[1] }
26 | CDISPOSITION cdisp { val[1] }
27 | ADDRESS addr_TOP { val[1] }
28 | MAILBOX mbox { val[1] }
29
30 datetime : day DIGIT ATOM DIGIT hour zone
31 # 0 1 2 3 4 5
32 # date month year
33 {
34 t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
35 (t + val[4] - val[5]).localtime
36 }
37
38 day : /* none */
39 | ATOM ','
40
41 hour : DIGIT ':' DIGIT
42 {
43 (val[0].to_i * 60 * 60) +
44 (val[2].to_i * 60)
45 }
46 | DIGIT ':' DIGIT ':' DIGIT
47 {
48 (val[0].to_i * 60 * 60) +
49 (val[2].to_i * 60) +
50 (val[4].to_i)
51 }
52
53 zone : ATOM
54 {
55 timezone_string_to_unixtime(val[0])
56 }
57
58 received : from by via with id for received_datetime
59 {
60 val
61 }
62
63 from : /* none */
64 | FROM received_domain
65 {
66 val[1]
67 }
68
69 by : /* none */
70 | BY received_domain
71 {
72 val[1]
73 }
74
75 received_domain
76 : domain
77 {
78 join_domain(val[0])
79 }
80 | domain '@' domain
81 {
82 join_domain(val[2])
83 }
84 | domain DOMLIT
85 {
86 join_domain(val[0])
87 }
88
89 via : /* none */
90 | VIA ATOM
91 {
92 val[1]
93 }
94
95 with : /* none */
96 {
97 []
98 }
99 | with WITH ATOM
100 {
101 val[0].push val[2]
102 val[0]
103 }
104
105 id : /* none */
106 | ID msgid
107 {
108 val[1]
109 }
110 | ID ATOM
111 {
112 val[1]
113 }
114
115 for : /* none */
116 | FOR received_addrspec
117 {
118 val[1]
119 }
120
121 received_addrspec
122 : routeaddr
123 {
124 val[0].spec
125 }
126 | spec
127 {
128 val[0].spec
129 }
130
131 received_datetime
132 : /* none */
133 | ';' datetime
134 {
135 val[1]
136 }
137
138 addrs_TOP : addrs
139 | group_bare
140 | addrs commas group_bare
141
142 addr_TOP : mbox
143 | group
144 | group_bare
145
146 retpath : addrs_TOP
147 | '<' '>' { [ Address.new(nil, nil) ] }
148
149 addrs : addr
150 {
151 val
152 }
153 | addrs commas addr
154 {
155 val[0].push val[2]
156 val[0]
157 }
158
159 addr : mbox
160 | group
161
162 mboxes : mbox
163 {
164 val
165 }
166 | mboxes commas mbox
167 {
168 val[0].push val[2]
169 val[0]
170 }
171
172 mbox : spec
173 | routeaddr
174 | addr_phrase routeaddr
175 {
176 val[1].phrase = Decoder.decode(val[0])
177 val[1]
178 }
179
180 group : group_bare ';'
181
182 group_bare: addr_phrase ':' mboxes
183 {
184 AddressGroup.new(val[0], val[2])
185 }
186 | addr_phrase ':' { AddressGroup.new(val[0], []) }
187
188 addr_phrase
189 : local_head { val[0].join('.') }
190 | addr_phrase local_head { val[0] << ' ' << val[1].join('.') }
191
192 routeaddr : '<' routes spec '>'
193 {
194 val[2].routes.replace val[1]
195 val[2]
196 }
197 | '<' spec '>'
198 {
199 val[1]
200 }
201
202 routes : at_domains ':'
203
204 at_domains: '@' domain { [ val[1].join('.') ] }
205 | at_domains ',' '@' domain { val[0].push val[3].join('.'); val[0] }
206
207 spec : local '@' domain { Address.new( val[0], val[2] ) }
208 | local { Address.new( val[0], nil ) }
209
210 local: local_head
211 | local_head '.' { val[0].push ''; val[0] }
212
213 local_head: word
214 { val }
215 | local_head dots word
216 {
217 val[1].times do
218 val[0].push ''
219 end
220 val[0].push val[2]
221 val[0]
222 }
223
224 domain : domword
225 { val }
226 | domain dots domword
227 {
228 val[1].times do
229 val[0].push ''
230 end
231 val[0].push val[2]
232 val[0]
233 }
234
235 dots : '.' { 0 }
236 | dots '.' { val[0] + 1 }
237
238 word : atom
239 | QUOTED
240 | DIGIT
241
242 domword : atom
243 | DOMLIT
244 | DIGIT
245
246 commas : ','
247 | commas ','
248
249 msgid : '<' spec '>'
250 {
251 val[1] = val[1].spec
252 val.join('')
253 }
254
255 keys : phrase { val }
256 | keys ',' phrase { val[0].push val[2]; val[0] }
257
258 phrase : word
259 | phrase word { val[0] << ' ' << val[1] }
260
261 enc : word
262 {
263 val.push nil
264 val
265 }
266 | word word
267 {
268 val
269 }
270
271 version : DIGIT '.' DIGIT
272 {
273 [ val[0].to_i, val[2].to_i ]
274 }
275
276 ctype : TOKEN '/' TOKEN params opt_semicolon
277 {
278 [ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
279 }
280 | TOKEN params opt_semicolon
281 {
282 [ val[0].downcase, nil, decode_params(val[1]) ]
283 }
284
285 params : /* none */
286 {
287 {}
288 }
289 | params ';' TOKEN '=' QUOTED
290 {
291 val[0][ val[2].downcase ] = ('"' + val[4].to_s + '"')
292 val[0]
293 }
294 | params ';' TOKEN '=' TOKEN
295 {
296 val[0][ val[2].downcase ] = val[4]
297 val[0]
298 }
299
300 cencode : TOKEN
301 {
302 val[0].downcase
303 }
304
305 cdisp : TOKEN params opt_semicolon
306 {
307 [ val[0].downcase, decode_params(val[1]) ]
308 }
309
310 opt_semicolon
311 :
312 | ';'
313
314 atom : ATOM
315 | FROM
316 | BY
317 | VIA
318 | WITH
319 | ID
320 | FOR
321
322 end
323
324
325 ---- header
326 #
327 # parser.rb
328 #
329 # Copyright (c) 1998-2007 Minero Aoki
330 #
331 # This program is free software.
332 # You can distribute/modify this program under the terms of
333 # the GNU Lesser General Public License version 2.1.
334 #
335
336 require 'tmail/scanner'
337 require 'tmail/utils'
338
339 ---- inner
340
341 include TextUtils
342
343 def self.parse( ident, str, cmt = nil )
344 str = special_quote_address(str) if ident.to_s =~ /M?ADDRESS/
345 new.parse(ident, str, cmt)
346 end
347
348 def self.special_quote_address(str) #:nodoc:
349 # Takes a string which is an address and adds quotation marks to special
350 # edge case methods that the RACC parser can not handle.
351 #
352 # Right now just handles two edge cases:
353 #
354 # Full stop as the last character of the display name:
355 # Mikel L. <mikel@me.com>
356 # Returns:
357 # "Mikel L." <mikel@me.com>
358 #
359 # Unquoted @ symbol in the display name:
360 # mikel@me.com <mikel@me.com>
361 # Returns:
362 # "mikel@me.com" <mikel@me.com>
363 #
364 # Any other address not matching these patterns just gets returned as is.
365 case
366 # This handles the missing "" in an older version of Apple Mail.app
367 # around the display name when the display name contains a '@'
368 # like 'mikel@me.com <mikel@me.com>'
369 # Just quotes it to: '"mikel@me.com" <mikel@me.com>'
370 when str =~ /\A([^"].+@.+[^"])\s(<.*?>)\Z/
371 return "\"#{$1}\" #{$2}"
372 # This handles cases where 'Mikel A. <mikel@me.com>' which is a trailing
373 # full stop before the address section. Just quotes it to
374 # '"Mikel A." <mikel@me.com>'
375 when str =~ /\A(.*?\.)\s(<.*?>)\s*\Z/
376 return "\"#{$1}\" #{$2}"
377 else
378 str
379 end
380 end
381
382 MAILP_DEBUG = false
383
384 def initialize
385 self.debug = MAILP_DEBUG
386 end
387
388 def debug=( flag )
389 @yydebug = flag && Racc_debug_parser
390 @scanner_debug = flag
391 end
392
393 def debug
394 @yydebug
395 end
396
397 def parse( ident, str, comments = nil )
398 @scanner = Scanner.new(str, ident, comments)
399 @scanner.debug = @scanner_debug
400 @first = [ident, ident]
401 result = yyparse(self, :parse_in)
402 comments.map! {|c| to_kcode(c) } if comments
403 result
404 end
405
406 private
407
408 def parse_in( &block )
409 yield @first
410 @scanner.scan(&block)
411 end
412
413 def on_error( t, val, vstack )
414 raise TMail::SyntaxError, "parse error on token #{racc_token2str t}"
415 end
416
@@ -0,0 +1,379
1 =begin rdoc
2
3 = Port class
4
5 =end
6 #--
7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
30 #++
31
32 require 'tmail/stringio'
33
34
35 module TMail
36
37 class Port
38 def reproducible?
39 false
40 end
41 end
42
43
44 ###
45 ### FilePort
46 ###
47
48 class FilePort < Port
49
50 def initialize( fname )
51 @filename = File.expand_path(fname)
52 super()
53 end
54
55 attr_reader :filename
56
57 alias ident filename
58
59 def ==( other )
60 other.respond_to?(:filename) and @filename == other.filename
61 end
62
63 alias eql? ==
64
65 def hash
66 @filename.hash
67 end
68
69 def inspect
70 "#<#{self.class}:#{@filename}>"
71 end
72
73 def reproducible?
74 true
75 end
76
77 def size
78 File.size @filename
79 end
80
81
82 def ropen( &block )
83 File.open(@filename, &block)
84 end
85
86 def wopen( &block )
87 File.open(@filename, 'w', &block)
88 end
89
90 def aopen( &block )
91 File.open(@filename, 'a', &block)
92 end
93
94
95 def read_all
96 ropen {|f|
97 return f.read
98 }
99 end
100
101
102 def remove
103 File.unlink @filename
104 end
105
106 def move_to( port )
107 begin
108 File.link @filename, port.filename
109 rescue Errno::EXDEV
110 copy_to port
111 end
112 File.unlink @filename
113 end
114
115 alias mv move_to
116
117 def copy_to( port )
118 if FilePort === port
119 copy_file @filename, port.filename
120 else
121 File.open(@filename) {|r|
122 port.wopen {|w|
123 while s = r.sysread(4096)
124 w.write << s
125 end
126 } }
127 end
128 end
129
130 alias cp copy_to
131
132 private
133
134 # from fileutils.rb
135 def copy_file( src, dest )
136 st = r = w = nil
137
138 File.open(src, 'rb') {|r|
139 File.open(dest, 'wb') {|w|
140 st = r.stat
141 begin
142 while true
143 w.write r.sysread(st.blksize)
144 end
145 rescue EOFError
146 end
147 } }
148 end
149
150 end
151
152
153 module MailFlags
154
155 def seen=( b )
156 set_status 'S', b
157 end
158
159 def seen?
160 get_status 'S'
161 end
162
163 def replied=( b )
164 set_status 'R', b
165 end
166
167 def replied?
168 get_status 'R'
169 end
170
171 def flagged=( b )
172 set_status 'F', b
173 end
174
175 def flagged?
176 get_status 'F'
177 end
178
179 private
180
181 def procinfostr( str, tag, true_p )
182 a = str.upcase.split(//)
183 a.push true_p ? tag : nil
184 a.delete tag unless true_p
185 a.compact.sort.join('').squeeze
186 end
187
188 end
189
190
191 class MhPort < FilePort
192
193 include MailFlags
194
195 private
196
197 def set_status( tag, flag )
198 begin
199 tmpfile = @filename + '.tmailtmp.' + $$.to_s
200 File.open(tmpfile, 'w') {|f|
201 write_status f, tag, flag
202 }
203 File.unlink @filename
204 File.link tmpfile, @filename
205 ensure
206 File.unlink tmpfile
207 end
208 end
209
210 def write_status( f, tag, flag )
211 stat = ''
212 File.open(@filename) {|r|
213 while line = r.gets
214 if line.strip.empty?
215 break
216 elsif m = /\AX-TMail-Status:/i.match(line)
217 stat = m.post_match.strip
218 else
219 f.print line
220 end
221 end
222
223 s = procinfostr(stat, tag, flag)
224 f.puts 'X-TMail-Status: ' + s unless s.empty?
225 f.puts
226
227 while s = r.read(2048)
228 f.write s
229 end
230 }
231 end
232
233 def get_status( tag )
234 File.foreach(@filename) {|line|
235 return false if line.strip.empty?
236 if m = /\AX-TMail-Status:/i.match(line)
237 return m.post_match.strip.include?(tag[0])
238 end
239 }
240 false
241 end
242
243 end
244
245
246 class MaildirPort < FilePort
247
248 def move_to_new
249 new = replace_dir(@filename, 'new')
250 File.rename @filename, new
251 @filename = new
252 end
253
254 def move_to_cur
255 new = replace_dir(@filename, 'cur')
256 File.rename @filename, new
257 @filename = new
258 end
259
260 def replace_dir( path, dir )
261 "#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
262 end
263 private :replace_dir
264
265
266 include MailFlags
267
268 private
269
270 MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
271
272 def set_status( tag, flag )
273 if m = MAIL_FILE.match(File.basename(@filename))
274 s, uniq, type, info, = m.to_a
275 return if type and type != '2' # do not change anything
276 newname = File.dirname(@filename) + '/' +
277 uniq + ':2,' + procinfostr(info.to_s, tag, flag)
278 else
279 newname = @filename + ':2,' + tag
280 end
281
282 File.link @filename, newname
283 File.unlink @filename
284 @filename = newname
285 end
286
287 def get_status( tag )
288 m = MAIL_FILE.match(File.basename(@filename)) or return false
289 m[2] == '2' and m[3].to_s.include?(tag[0])
290 end
291
292 end
293
294
295 ###
296 ### StringPort
297 ###
298
299 class StringPort < Port
300
301 def initialize( str = '' )
302 @buffer = str
303 super()
304 end
305
306 def string
307 @buffer
308 end
309
310 def to_s
311 @buffer.dup
312 end
313
314 alias read_all to_s
315
316 def size
317 @buffer.size
318 end
319
320 def ==( other )
321 StringPort === other and @buffer.equal? other.string
322 end
323
324 alias eql? ==
325
326 def hash
327 @buffer.object_id.hash
328 end
329
330 def inspect
331 "#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_id}>"
332 end
333
334 def reproducible?
335 true
336 end
337
338 def ropen( &block )
339 @buffer or raise Errno::ENOENT, "#{inspect} is already removed"
340 StringInput.open(@buffer, &block)
341 end
342
343 def wopen( &block )
344 @buffer = ''
345 StringOutput.new(@buffer, &block)
346 end
347
348 def aopen( &block )
349 @buffer ||= ''
350 StringOutput.new(@buffer, &block)
351 end
352
353 def remove
354 @buffer = nil
355 end
356
357 alias rm remove
358
359 def copy_to( port )
360 port.wopen {|f|
361 f.write @buffer
362 }
363 end
364
365 alias cp copy_to
366
367 def move_to( port )
368 if StringPort === port
369 str = @buffer
370 port.instance_eval { @buffer = str }
371 else
372 copy_to port
373 end
374 remove
375 end
376
377 end
378
379 end # module TMail
@@ -0,0 +1,164
1 =begin rdoc
2
3 = Quoting methods
4
5 =end
6 module TMail
7 class Mail
8 def subject(to_charset = 'utf-8')
9 Unquoter.unquote_and_convert_to(quoted_subject, to_charset)
10 end
11
12 def unquoted_body(to_charset = 'utf-8')
13 from_charset = charset
14 case (content_transfer_encoding || "7bit").downcase
15 when "quoted-printable"
16 # the default charset is set to iso-8859-1 instead of 'us-ascii'.
17 # This is needed as many mailer do not set the charset but send in ISO. This is only used if no charset is set.
18 if !from_charset.blank? && from_charset.downcase == 'us-ascii'
19 from_charset = 'iso-8859-1'
20 end
21
22 Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
23 to_charset, from_charset, true)
24 when "base64"
25 Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
26 from_charset)
27 when "7bit", "8bit"
28 Unquoter.convert_to(quoted_body, to_charset, from_charset)
29 when "binary"
30 quoted_body
31 else
32 quoted_body
33 end
34 end
35
36 def body(to_charset = 'utf-8', &block)
37 attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" }
38
39 if multipart?
40 parts.collect { |part|
41 header = part["content-type"]
42
43 if part.multipart?
44 part.body(to_charset, &attachment_presenter)
45 elsif header.nil?
46 ""
47 elsif !attachment?(part)
48 part.unquoted_body(to_charset)
49 else
50 attachment_presenter.call(header["name"] || "(unnamed)")
51 end
52 }.join
53 else
54 unquoted_body(to_charset)
55 end
56 end
57 end
58
59 class Attachment
60
61 include TextUtils
62
63 def quoted?(string)
64 !!((string =~ /.+'\w\w'.+/) || (string =~ /=\?.+\?.\?.+\?=/))
65 end
66
67 # Only unquote if quoted
68 def original_filename(to_charset = 'utf-8')
69 if quoted?(quoted_filename)
70 Unquoter.unquote_and_convert_to(quoted_filename, to_charset).chomp
71 else
72 quoted_filename
73 end
74 end
75 end
76
77 class Unquoter
78 class << self
79 def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false)
80 return "" if text.nil?
81 text.gsub!(/\?=(\s*)=\?/, '?==?') # Remove whitespaces between 'encoded-word's
82 text.gsub(/(.*?)(?:(?:=\?(.*?)\?(.)\?(.*?)\?=)|$)/) do
83 before = $1
84 from_charset = $2
85 quoting_method = $3
86 text = $4
87
88 before = convert_to(before, to_charset, from_charset) if before.length > 0
89 before + case quoting_method
90 when "q", "Q" then
91 unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores)
92 when "b", "B" then
93 unquote_base64_and_convert_to(text, to_charset, from_charset)
94 when nil then
95 # will be nil at the end of the string, due to the nature of
96 # the regex used.
97 ""
98 else
99 raise "unknown quoting method #{quoting_method.inspect}"
100 end
101 end
102 end
103
104 def convert_to_with_fallback_on_iso_8859_1(text, to, from)
105 return text if to == 'utf-8' and text.isutf8
106
107 if from.blank? and !text.is_binary_data?
108 from = CharDet.detect(text)['encoding']
109
110 # Chardet ususally detects iso-8859-2 (aka windows-1250), but the text is
111 # iso-8859-1 (aka windows-1252 and Latin1). http://en.wikipedia.org/wiki/ISO/IEC_8859-2
112 # This can cause unwanted characters, like ŕ instead of à.
113 # (I know, could be a very bad decision...)
114 from = 'iso-8859-1' if from =~ /iso-8859-2/i
115 end
116
117 begin
118 convert_to_without_fallback_on_iso_8859_1(text, to, from)
119 rescue Iconv::InvalidCharacter
120 unless from == 'iso-8859-1'
121 from = 'iso-8859-1'
122 retry
123 end
124 end
125 end
126
127 def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false)
128 text = text.gsub(/_/, " ") unless preserve_underscores
129 text = text.gsub(/\r\n|\r/, "\n") # normalize newlines
130 convert_to(text.unpack("M*").first, to, from)
131 end
132
133 def unquote_base64_and_convert_to(text, to, from)
134 convert_to(Base64.decode(text), to, from)
135 end
136
137 begin
138 require 'iconv'
139 def convert_to(text, to, from)
140 return text unless to && from
141 text ? Iconv.iconv(to, from, text).first : ""
142 rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
143 # the 'from' parameter specifies a charset other than what the text
144 # actually is...not much we can do in this case but just return the
145 # unconverted text.
146 #
147 # Ditto if either parameter represents an unknown charset, like
148 # X-UNKNOWN.
149 text
150 end
151 rescue LoadError
152 # Not providing quoting support
153 def convert_to(text, to, from)
154 warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})"
155 text
156 end
157 end
158
159 alias_method :convert_to_without_fallback_on_iso_8859_1, :convert_to
160 alias_method :convert_to, :convert_to_with_fallback_on_iso_8859_1
161
162 end
163 end
164 end
@@ -0,0 +1,58
1 #:stopdoc:
2 require 'rbconfig'
3
4 # Attempts to require anative extension.
5 # Falls back to pure-ruby version, if it fails.
6 #
7 # This uses Config::CONFIG['arch'] from rbconfig.
8
9 def require_arch(fname)
10 arch = Config::CONFIG['arch']
11 begin
12 path = File.join("tmail", arch, fname)
13 require path
14 rescue LoadError => e
15 # try pre-built Windows binaries
16 if arch =~ /mswin/
17 require File.join("tmail", 'mswin32', fname)
18 else
19 raise e
20 end
21 end
22 end
23
24
25 # def require_arch(fname)
26 # dext = Config::CONFIG['DLEXT']
27 # begin
28 # if File.extname(fname) == dext
29 # path = fname
30 # else
31 # path = File.join("tmail","#{fname}.#{dext}")
32 # end
33 # require path
34 # rescue LoadError => e
35 # begin
36 # arch = Config::CONFIG['arch']
37 # path = File.join("tmail", arch, "#{fname}.#{dext}")
38 # require path
39 # rescue LoadError
40 # case path
41 # when /i686/
42 # path.sub!('i686', 'i586')
43 # when /i586/
44 # path.sub!('i586', 'i486')
45 # when /i486/
46 # path.sub!('i486', 'i386')
47 # else
48 # begin
49 # require fname + '.rb'
50 # rescue LoadError
51 # raise e
52 # end
53 # end
54 # retry
55 # end
56 # end
57 # end
58 #:startdoc: No newline at end of file
@@ -0,0 +1,49
1 =begin rdoc
2
3 = Scanner for TMail
4
5 =end
6 #--
7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
30 #++
31 #:stopdoc:
32 #require 'tmail/require_arch'
33 require 'tmail/utils'
34 require 'tmail/config'
35
36 module TMail
37 # NOTE: It woiuld be nice if these two libs could boith be called "tmailscanner", and
38 # the native extension would have precedence. However RubyGems boffs that up b/c
39 # it does not gaurantee load_path order.
40 begin
41 raise LoadError, 'Turned off native extentions by user choice' if ENV['NORUBYEXT']
42 require('tmail/tmailscanner') # c extension
43 Scanner = TMailScanner
44 rescue LoadError
45 require 'tmail/scanner_r'
46 Scanner = TMailScanner
47 end
48 end
49 #:stopdoc: No newline at end of file
@@ -0,0 +1,262
1 # encoding: us-ascii
2 # scanner_r.rb
3 #
4 #--
5 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining
8 # a copy of this software and associated documentation files (the
9 # "Software"), to deal in the Software without restriction, including
10 # without limitation the rights to use, copy, modify, merge, publish,
11 # distribute, sublicense, and/or sell copies of the Software, and to
12 # permit persons to whom the Software is furnished to do so, subject to
13 # the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be
16 # included in all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 #
26 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
27 # with permission of Minero Aoki.
28 #++
29 #:stopdoc:
30 require 'tmail/config'
31
32 module TMail
33
34 class TMailScanner
35
36 Version = '1.2.3'
37 Version.freeze
38
39 MIME_HEADERS = {
40 :CTYPE => true,
41 :CENCODING => true,
42 :CDISPOSITION => true
43 }
44
45 alnum = 'a-zA-Z0-9'
46 atomsyms = %q[ _#!$%&`'*+-{|}~^/=? ].strip
47 tokensyms = %q[ _#!$%&`'*+-{|}~^@. ].strip
48 atomchars = alnum + Regexp.quote(atomsyms)
49 tokenchars = alnum + Regexp.quote(tokensyms)
50 iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
51
52 eucstr = "(?:[\xa1-\xfe][\xa1-\xfe])+"
53 sjisstr = "(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+"
54 utf8str = "(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+"
55
56 quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
57 domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
58 comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
59
60 quoted_without_iso2022 = /\A[^\\"]+/n
61 domlit_without_iso2022 = /\A[^\\\]]+/n
62 comment_without_iso2022 = /\A[^\\()]+/n
63
64 PATTERN_TABLE = {}
65 PATTERN_TABLE['EUC'] =
66 [
67 /\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
68 /\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
69 quoted_with_iso2022,
70 domlit_with_iso2022,
71 comment_with_iso2022
72 ]
73 PATTERN_TABLE['SJIS'] =
74 [
75 /\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
76 /\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
77 quoted_with_iso2022,
78 domlit_with_iso2022,
79 comment_with_iso2022
80 ]
81 PATTERN_TABLE['UTF8'] =
82 [
83 /\A(?:[#{atomchars}]+|#{utf8str})+/n,
84 /\A(?:[#{tokenchars}]+|#{utf8str})+/n,
85 quoted_without_iso2022,
86 domlit_without_iso2022,
87 comment_without_iso2022
88 ]
89 PATTERN_TABLE['NONE'] =
90 [
91 /\A[#{atomchars}]+/n,
92 /\A[#{tokenchars}]+/n,
93 quoted_without_iso2022,
94 domlit_without_iso2022,
95 comment_without_iso2022
96 ]
97
98
99 def initialize( str, scantype, comments )
100 init_scanner str
101 @comments = comments || []
102 @debug = false
103
104 # fix scanner mode
105 @received = (scantype == :RECEIVED)
106 @is_mime_header = MIME_HEADERS[scantype]
107
108 atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[TMail.KCODE]
109 @word_re = (MIME_HEADERS[scantype] ? token : atom)
110 end
111
112 attr_accessor :debug
113
114 def scan( &block )
115 if @debug
116 scan_main do |arr|
117 s, v = arr
118 printf "%7d %-10s %s\n",
119 rest_size(),
120 s.respond_to?(:id2name) ? s.id2name : s.inspect,
121 v.inspect
122 yield arr
123 end
124 else
125 scan_main(&block)
126 end
127 end
128
129 private
130
131 RECV_TOKEN = {
132 'from' => :FROM,
133 'by' => :BY,
134 'via' => :VIA,
135 'with' => :WITH,
136 'id' => :ID,
137 'for' => :FOR
138 }
139
140 def scan_main
141 until eof?
142 if skip(/\A[\n\r\t ]+/n) # LWSP
143 break if eof?
144 end
145
146 if s = readstr(@word_re)
147 if @is_mime_header
148 yield [:TOKEN, s]
149 else
150 # atom
151 if /\A\d+\z/ === s
152 yield [:DIGIT, s]
153 elsif @received
154 yield [RECV_TOKEN[s.downcase] || :ATOM, s]
155 else
156 yield [:ATOM, s]
157 end
158 end
159
160 elsif skip(/\A"/)
161 yield [:QUOTED, scan_quoted_word()]
162
163 elsif skip(/\A\[/)
164 yield [:DOMLIT, scan_domain_literal()]
165
166 elsif skip(/\A\(/)
167 @comments.push scan_comment()
168
169 else
170 c = readchar()
171 yield [c, c]
172 end
173 end
174
175 yield [false, '$']
176 end
177
178 def scan_quoted_word
179 scan_qstr(@quoted_re, /\A"/, 'quoted-word')
180 end
181
182 def scan_domain_literal
183 '[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
184 end
185
186 def scan_qstr( pattern, terminal, type )
187 result = ''
188 until eof?
189 if s = readstr(pattern) then result << s
190 elsif skip(terminal) then return result
191 elsif skip(/\A\\/) then result << readchar()
192 else
193 raise "TMail FATAL: not match in #{type}"
194 end
195 end
196 scan_error! "found unterminated #{type}"
197 end
198
199 def scan_comment
200 result = ''
201 nest = 1
202 content = @comment_re
203
204 until eof?
205 if s = readstr(content) then result << s
206 elsif skip(/\A\)/) then nest -= 1
207 return result if nest == 0
208 result << ')'
209 elsif skip(/\A\(/) then nest += 1
210 result << '('
211 elsif skip(/\A\\/) then result << readchar()
212 else
213 raise 'TMail FATAL: not match in comment'
214 end
215 end
216 scan_error! 'found unterminated comment'
217 end
218
219 # string scanner
220
221 def init_scanner( str )
222 @src = str
223 end
224
225 def eof?
226 @src.empty?
227 end
228
229 def rest_size
230 @src.size
231 end
232
233 def readstr( re )
234 if m = re.match(@src)
235 @src = m.post_match
236 m[0]
237 else
238 nil
239 end
240 end
241
242 def readchar
243 readstr(/\A./)
244 end
245
246 def skip( re )
247 if m = re.match(@src)
248 @src = m.post_match
249 true
250 else
251 false
252 end
253 end
254
255 def scan_error!( msg )
256 raise SyntaxError, msg
257 end
258
259 end
260
261 end # module TMail
262 #:startdoc:
@@ -0,0 +1,280
1 # encoding: utf-8
2 =begin rdoc
3
4 = String handling class
5
6 =end
7 #--
8 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
9 #
10 # Permission is hereby granted, free of charge, to any person obtaining
11 # a copy of this software and associated documentation files (the
12 # "Software"), to deal in the Software without restriction, including
13 # without limitation the rights to use, copy, modify, merge, publish,
14 # distribute, sublicense, and/or sell copies of the Software, and to
15 # permit persons to whom the Software is furnished to do so, subject to
16 # the following conditions:
17 #
18 # The above copyright notice and this permission notice shall be
19 # included in all copies or substantial portions of the Software.
20 #
21 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 #
29 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
30 # with permission of Minero Aoki.
31 #++
32
33 class StringInput#:nodoc:
34
35 include Enumerable
36
37 class << self
38
39 def new( str )
40 if block_given?
41 begin
42 f = super
43 yield f
44 ensure
45 f.close if f
46 end
47 else
48 super
49 end
50 end
51
52 alias open new
53
54 end
55
56 def initialize( str )
57 @src = str
58 @pos = 0
59 @closed = false
60 @lineno = 0
61 end
62
63 attr_reader :lineno
64
65 def string
66 @src
67 end
68
69 def inspect
70 "#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>"
71 end
72
73 def close
74 stream_check!
75 @pos = nil
76 @closed = true
77 end
78
79 def closed?
80 @closed
81 end
82
83 def pos
84 stream_check!
85 [@pos, @src.size].min
86 end
87
88 alias tell pos
89
90 def seek( offset, whence = IO::SEEK_SET )
91 stream_check!
92 case whence
93 when IO::SEEK_SET
94 @pos = offset
95 when IO::SEEK_CUR
96 @pos += offset
97 when IO::SEEK_END
98 @pos = @src.size - offset
99 else
100 raise ArgumentError, "unknown seek flag: #{whence}"
101 end
102 @pos = 0 if @pos < 0
103 @pos = [@pos, @src.size + 1].min
104 offset
105 end
106
107 def rewind
108 stream_check!
109 @pos = 0
110 end
111
112 def eof?
113 stream_check!
114 @pos > @src.size
115 end
116
117 def each( &block )
118 stream_check!
119 begin
120 @src.each(&block)
121 ensure
122 @pos = 0
123 end
124 end
125
126 def gets
127 stream_check!
128 if idx = @src.index(?\n, @pos)
129 idx += 1 # "\n".size
130 line = @src[ @pos ... idx ]
131 @pos = idx
132 @pos += 1 if @pos == @src.size
133 else
134 line = @src[ @pos .. -1 ]
135 @pos = @src.size + 1
136 end
137 @lineno += 1
138
139 line
140 end
141
142 def getc
143 stream_check!
144 ch = @src[@pos]
145 @pos += 1
146 @pos += 1 if @pos == @src.size
147 ch
148 end
149
150 def read( len = nil )
151 stream_check!
152 return read_all unless len
153 str = @src[@pos, len]
154 @pos += len
155 @pos += 1 if @pos == @src.size
156 str
157 end
158
159 alias sysread read
160
161 def read_all
162 stream_check!
163 return nil if eof?
164 rest = @src[@pos ... @src.size]
165 @pos = @src.size + 1
166 rest
167 end
168
169 def stream_check!
170 @closed and raise IOError, 'closed stream'
171 end
172
173 end
174
175
176 class StringOutput#:nodoc:
177
178 class << self
179
180 def new( str = '' )
181 if block_given?
182 begin
183 f = super
184 yield f
185 ensure
186 f.close if f
187 end
188 else
189 super
190 end
191 end
192
193 alias open new
194
195 end
196
197 def initialize( str = '' )
198 @dest = str
199 @closed = false
200 end
201
202 def close
203 @closed = true
204 end
205
206 def closed?
207 @closed
208 end
209
210 def string
211 @dest
212 end
213
214 alias value string
215 alias to_str string
216
217 def size
218 @dest.size
219 end
220
221 alias pos size
222
223 def inspect
224 "#<#{self.class}:#{@dest ? 'open' : 'closed'},#{object_id}>"
225 end
226
227 def print( *args )
228 stream_check!
229 raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty?
230 args.each do |s|
231 raise ArgumentError, 'nil not allowed' if s.nil?
232 @dest << s.to_s
233 end
234 nil
235 end
236
237 def puts( *args )
238 stream_check!
239 args.each do |str|
240 @dest << (s = str.to_s)
241 @dest << "\n" unless s[-1] == ?\n
242 end
243 @dest << "\n" if args.empty?
244 nil
245 end
246
247 def putc( ch )
248 stream_check!
249 @dest << ch.chr
250 nil
251 end
252
253 def printf( *args )
254 stream_check!
255 @dest << sprintf(*args)
256 nil
257 end
258
259 def write( str )
260 stream_check!
261 s = str.to_s
262 @dest << s
263 s.size
264 end
265
266 alias syswrite write
267
268 def <<( str )
269 stream_check!
270 @dest << str.to_s
271 self
272 end
273
274 private
275
276 def stream_check!
277 @closed and raise IOError, 'closed stream'
278 end
279
280 end
@@ -0,0 +1,362
1 # encoding: us-ascii
2 #--
3 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 #
24 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
25 # with permission of Minero Aoki.
26 #++
27
28 # = TMail - The EMail Swiss Army Knife for Ruby
29 #
30 # The TMail library provides you with a very complete way to handle and manipulate EMails
31 # from within your Ruby programs.
32 #
33 # Used as the backbone for email handling by the Ruby on Rails and Nitro web frameworks as
34 # well as a bunch of other Ruby apps including the Ruby-Talk mailing list to newsgroup email
35 # gateway, it is a proven and reliable email handler that won't let you down.
36 #
37 # Originally created by Minero Aoki, TMail has been recently picked up by Mikel Lindsaar and
38 # is being actively maintained. Numerous backlogged bug fixes have been applied as well as
39 # Ruby 1.9 compatibility and a swath of documentation to boot.
40 #
41 # TMail allows you to treat an email totally as an object and allow you to get on with your
42 # own programming without having to worry about crafting the perfect email address validation
43 # parser, or assembling an email from all it's component parts.
44 #
45 # TMail handles the most complex part of the email - the header. It generates and parses
46 # headers and provides you with instant access to their innards through simple and logically
47 # named accessor and setter methods.
48 #
49 # TMail also provides a wrapper to Net/SMTP as well as Unix Mailbox handling methods to
50 # directly read emails from your unix mailbox, parse them and use them.
51 #
52 # Following is the comprehensive list of methods to access TMail::Mail objects. You can also
53 # check out TMail::Mail, TMail::Address and TMail::Headers for other lists.
54 module TMail
55
56 # Provides an exception to throw on errors in Syntax within TMail's parsers
57 class SyntaxError < StandardError; end
58
59 # Provides a new email boundary to separate parts of the email. This is a random
60 # string based off the current time, so should be fairly unique.
61 #
62 # For Example:
63 #
64 # TMail.new_boundary
65 # #=> "mimepart_47bf656968207_25a8fbb80114"
66 # TMail.new_boundary
67 # #=> "mimepart_47bf66051de4_25a8fbb80240"
68 def TMail.new_boundary
69 'mimepart_' + random_tag
70 end
71
72 # Provides a new email message ID. You can use this to generate unique email message
73 # id's for your email so you can track them.
74 #
75 # Optionally takes a fully qualified domain name (default to the current hostname
76 # returned by Socket.gethostname) that will be appended to the message ID.
77 #
78 # For Example:
79 #
80 # email.message_id = TMail.new_message_id
81 # #=> "<47bf66845380e_25a8fbb80332@baci.local.tmail>"
82 # email.to_s
83 # #=> "Message-Id: <47bf668b633f1_25a8fbb80475@baci.local.tmail>\n\n"
84 # email.message_id = TMail.new_message_id("lindsaar.net")
85 # #=> "<47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>"
86 # email.to_s
87 # #=> "Message-Id: <47bf668b633f1_25a8fbb80475@lindsaar.net.tmail>\n\n"
88 def TMail.new_message_id( fqdn = nil )
89 fqdn ||= ::Socket.gethostname
90 "<#{random_tag()}@#{fqdn}.tmail>"
91 end
92
93 #:stopdoc:
94 def TMail.random_tag #:nodoc:
95 @uniq += 1
96 t = Time.now
97 sprintf('%x%x_%x%x%d%x',
98 t.to_i, t.tv_usec,
99 $$, Thread.current.object_id, @uniq, rand(255))
100 end
101 private_class_method :random_tag
102
103 @uniq = 0
104
105 #:startdoc:
106
107 # Text Utils provides a namespace to define TOKENs, ATOMs, PHRASEs and CONTROL characters that
108 # are OK per RFC 2822.
109 #
110 # It also provides methods you can call to determine if a string is safe
111 module TextUtils
112
113 aspecial = %Q|()<>[]:;.\\,"|
114 tspecial = %Q|()<>[];:\\,"/?=|
115 lwsp = %Q| \t\r\n|
116 control = %Q|\x00-\x1f\x7f-\xff|
117
118 CONTROL_CHAR = /[#{control}]/n
119 ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
120 PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
121 TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
122
123 # Returns true if the string supplied is free from characters not allowed as an ATOM
124 def atom_safe?( str )
125 not ATOM_UNSAFE === str
126 end
127
128 # If the string supplied has ATOM unsafe characters in it, will return the string quoted
129 # in double quotes, otherwise returns the string unmodified
130 def quote_atom( str )
131 (ATOM_UNSAFE === str) ? dquote(str) : str
132 end
133
134 # If the string supplied has PHRASE unsafe characters in it, will return the string quoted
135 # in double quotes, otherwise returns the string unmodified
136 def quote_phrase( str )
137 (PHRASE_UNSAFE === str) ? dquote(str) : str
138 end
139
140 # Returns true if the string supplied is free from characters not allowed as a TOKEN
141 def token_safe?( str )
142 not TOKEN_UNSAFE === str
143 end
144
145 # If the string supplied has TOKEN unsafe characters in it, will return the string quoted
146 # in double quotes, otherwise returns the string unmodified
147 def quote_token( str )
148 (TOKEN_UNSAFE === str) ? dquote(str) : str
149 end
150
151 # Wraps supplied string in double quotes unless it is already wrapped
152 # Returns double quoted string
153 def dquote( str ) #:nodoc:
154 unless str =~ /^".*?"$/
155 '"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"'
156 else
157 str
158 end
159 end
160 private :dquote
161
162 # Unwraps supplied string from inside double quotes
163 # Returns unquoted string
164 def unquote( str )
165 str =~ /^"(.*?)"$/m ? $1 : str
166 end
167
168 # Provides a method to join a domain name by it's parts and also makes it
169 # ATOM safe by quoting it as needed
170 def join_domain( arr )
171 arr.map {|i|
172 if /\A\[.*\]\z/ === i
173 i
174 else
175 quote_atom(i)
176 end
177 }.join('.')
178 end
179
180 #:stopdoc:
181 ZONESTR_TABLE = {
182 'jst' => 9 * 60,
183 'eet' => 2 * 60,
184 'bst' => 1 * 60,
185 'met' => 1 * 60,
186 'gmt' => 0,
187 'utc' => 0,
188 'ut' => 0,
189 'nst' => -(3 * 60 + 30),
190 'ast' => -4 * 60,
191 'edt' => -4 * 60,
192 'est' => -5 * 60,
193 'cdt' => -5 * 60,
194 'cst' => -6 * 60,
195 'mdt' => -6 * 60,
196 'mst' => -7 * 60,
197 'pdt' => -7 * 60,
198 'pst' => -8 * 60,
199 'a' => -1 * 60,
200 'b' => -2 * 60,
201 'c' => -3 * 60,
202 'd' => -4 * 60,
203 'e' => -5 * 60,
204 'f' => -6 * 60,
205 'g' => -7 * 60,
206 'h' => -8 * 60,
207 'i' => -9 * 60,
208 # j not use
209 'k' => -10 * 60,
210 'l' => -11 * 60,
211 'm' => -12 * 60,
212 'n' => 1 * 60,
213 'o' => 2 * 60,
214 'p' => 3 * 60,
215 'q' => 4 * 60,
216 'r' => 5 * 60,
217 's' => 6 * 60,
218 't' => 7 * 60,
219 'u' => 8 * 60,
220 'v' => 9 * 60,
221 'w' => 10 * 60,
222 'x' => 11 * 60,
223 'y' => 12 * 60,
224 'z' => 0 * 60
225 }
226 #:startdoc:
227
228 # Takes a time zone string from an EMail and converts it to Unix Time (seconds)
229 def timezone_string_to_unixtime( str )
230 if m = /([\+\-])(\d\d?)(\d\d)/.match(str)
231 sec = (m[2].to_i * 60 + m[3].to_i) * 60
232 m[1] == '-' ? -sec : sec
233 else
234 min = ZONESTR_TABLE[str.downcase] or
235 raise SyntaxError, "wrong timezone format '#{str}'"
236 min * 60
237 end
238 end
239
240 #:stopdoc:
241 WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG )
242 MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun
243 Jul Aug Sep Oct Nov Dec TMailBUG )
244
245 def time2str( tm )
246 # [ruby-list:7928]
247 gmt = Time.at(tm.to_i)
248 gmt.gmtime
249 offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i
250
251 # DO NOT USE strftime: setlocale() breaks it
252 sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
253 WDAY[tm.wday], tm.mday, MONTH[tm.month],
254 tm.year, tm.hour, tm.min, tm.sec,
255 *(offset / 60).divmod(60)
256 end
257
258
259 MESSAGE_ID = /<[^\@>]+\@[^>]+>/
260
261 def message_id?( str )
262 MESSAGE_ID === str
263 end
264
265
266 MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
267
268 def mime_encoded?( str )
269 MIME_ENCODED === str
270 end
271
272
273 def decode_params( hash )
274 new = Hash.new
275 encoded = nil
276 hash.each do |key, value|
277 if m = /\*(?:(\d+)\*)?\z/.match(key)
278 ((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value
279 else
280 new[key] = to_kcode(value)
281 end
282 end
283 if encoded
284 encoded.each do |key, strings|
285 new[key] = decode_RFC2231(strings.join(''))
286 end
287 end
288
289 new
290 end
291
292 NKF_FLAGS = {
293 'EUC' => '-e -m',
294 'SJIS' => '-s -m'
295 }
296
297 def to_kcode( str )
298 flag = NKF_FLAGS[TMail.KCODE] or return str
299 NKF.nkf(flag, str)
300 end
301
302 RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
303
304 def decode_RFC2231( str )
305 m = RFC2231_ENCODED.match(str) or return str
306 begin
307 to_kcode(m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
308 rescue
309 m.post_match.gsub(/%[\da-f]{2}/in, "")
310 end
311 end
312
313 def quote_boundary
314 # Make sure the Content-Type boundary= parameter is quoted if it contains illegal characters
315 # (to ensure any special characters in the boundary text are escaped from the parser
316 # (such as = in MS Outlook's boundary text))
317 if @body =~ /^(.*)boundary=(.*)$/m
318 preamble = $1
319 remainder = $2
320 if remainder =~ /;/
321 remainder =~ /^(.*?)(;.*)$/m
322 boundary_text = $1
323 post = $2.chomp
324 else
325 boundary_text = remainder.chomp
326 end
327 if boundary_text =~ /[\/\?\=]/
328 boundary_text = "\"#{boundary_text}\"" unless boundary_text =~ /^".*?"$/
329 @body = "#{preamble}boundary=#{boundary_text}#{post}"
330 end
331 end
332 end
333
334 # AppleMail generates illegal character contained Content-Type parameter like:
335 # name==?ISO-2022-JP?B?...=?=
336 # so quote. (This case is only value fits in one line.)
337 def quote_unquoted_bencode
338 @body = @body.gsub(%r"(;\s+[-a-z]+=)(=\?.+?)([;\r\n ]|\z)"m) {
339 head, should_quoted, tail = $~.captures
340 # head: "; name="
341 # should_quoted: "=?ISO-2022-JP?B?...=?="
342
343 head << quote_token(should_quoted) << tail
344 }
345 end
346
347 # AppleMail generates name=filename attributes in the content type that
348 # contain spaces. Need to handle this so the TMail Parser can.
349 def quote_unquoted_name
350 @body = @body.gsub(%r|(name=)([\w\s.]+)(.*)|m) {
351 head, should_quoted, tail = $~.captures
352 # head: "; name="
353 # should_quoted: "=?ISO-2022-JP?B?...=?="
354 head << quote_token(should_quoted) << tail
355 }
356 end
357
358 #:startdoc:
359
360 end
361
362 end
This diff has been collapsed as it changes many lines, (504 lines changed) Show them Hide them
@@ -0,0 +1,504
1 GNU LESSER GENERAL PUBLIC LICENSE
2 Version 2.1, February 1999
3
4 Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
6 Everyone is permitted to copy and distribute verbatim copies
7 of this license document, but changing it is not allowed.
8
9 [This is the first released version of the Lesser GPL. It also counts
10 as the successor of the GNU Library Public License, version 2, hence
11 the version number 2.1.]
12
13 Preamble
14
15 The licenses for most software are designed to take away your
16 freedom to share and change it. By contrast, the GNU General Public
17 Licenses are intended to guarantee your freedom to share and change
18 free software--to make sure the software is free for all its users.
19
20 This license, the Lesser General Public License, applies to some
21 specially designated software packages--typically libraries--of the
22 Free Software Foundation and other authors who decide to use it. You
23 can use it too, but we suggest you first think carefully about whether
24 this license or the ordinary General Public License is the better
25 strategy to use in any particular case, based on the explanations below.
26
27 When we speak of free software, we are referring to freedom of use,
28 not price. Our General Public Licenses are designed to make sure that
29 you have the freedom to distribute copies of free software (and charge
30 for this service if you wish); that you receive source code or can get
31 it if you want it; that you can change the software and use pieces of
32 it in new free programs; and that you are informed that you can do
33 these things.
34
35 To protect your rights, we need to make restrictions that forbid
36 distributors to deny you these rights or to ask you to surrender these
37 rights. These restrictions translate to certain responsibilities for
38 you if you distribute copies of the library or if you modify it.
39
40 For example, if you distribute copies of the library, whether gratis
41 or for a fee, you must give the recipients all the rights that we gave
42 you. You must make sure that they, too, receive or can get the source
43 code. If you link other code with the library, you must provide
44 complete object files to the recipients, so that they can relink them
45 with the library after making changes to the library and recompiling
46 it. And you must show them these terms so they know their rights.
47
48 We protect your rights with a two-step method: (1) we copyright the
49 library, and (2) we offer you this license, which gives you legal
50 permission to copy, distribute and/or modify the library.
51
52 To protect each distributor, we want to make it very clear that
53 there is no warranty for the free library. Also, if the library is
54 modified by someone else and passed on, the recipients should know
55 that what they have is not the original version, so that the original
56 author's reputation will not be affected by problems that might be
57 introduced by others.
58
59 Finally, software patents pose a constant threat to the existence of
60 any free program. We wish to make sure that a company cannot
61 effectively restrict the users of a free program by obtaining a
62 restrictive license from a patent holder. Therefore, we insist that
63 any patent license obtained for a version of the library must be
64 consistent with the full freedom of use specified in this license.
65
66 Most GNU software, including some libraries, is covered by the
67 ordinary GNU General Public License. This license, the GNU Lesser
68 General Public License, applies to certain designated libraries, and
69 is quite different from the ordinary General Public License. We use
70 this license for certain libraries in order to permit linking those
71 libraries into non-free programs.
72
73 When a program is linked with a library, whether statically or using
74 a shared library, the combination of the two is legally speaking a
75 combined work, a derivative of the original library. The ordinary
76 General Public License therefore permits such linking only if the
77 entire combination fits its criteria of freedom. The Lesser General
78 Public License permits more lax criteria for linking other code with
79 the library.
80
81 We call this license the "Lesser" General Public License because it
82 does Less to protect the user's freedom than the ordinary General
83 Public License. It also provides other free software developers Less
84 of an advantage over competing non-free programs. These disadvantages
85 are the reason we use the ordinary General Public License for many
86 libraries. However, the Lesser license provides advantages in certain
87 special circumstances.
88
89 For example, on rare occasions, there may be a special need to
90 encourage the widest possible use of a certain library, so that it becomes
91 a de-facto standard. To achieve this, non-free programs must be
92 allowed to use the library. A more frequent case is that a free
93 library does the same job as widely used non-free libraries. In this
94 case, there is little to gain by limiting the free library to free
95 software only, so we use the Lesser General Public License.
96
97 In other cases, permission to use a particular library in non-free
98 programs enables a greater number of people to use a large body of
99 free software. For example, permission to use the GNU C Library in
100 non-free programs enables many more people to use the whole GNU
101 operating system, as well as its variant, the GNU/Linux operating
102 system.
103
104 Although the Lesser General Public License is Less protective of the
105 users' freedom, it does ensure that the user of a program that is
106 linked with the Library has the freedom and the wherewithal to run
107 that program using a modified version of the Library.
108
109 The precise terms and conditions for copying, distribution and
110 modification follow. Pay close attention to the difference between a
111 "work based on the library" and a "work that uses the library". The
112 former contains code derived from the library, whereas the latter must
113 be combined with the library in order to run.
114
115 GNU LESSER GENERAL PUBLIC LICENSE
116 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
117
118 0. This License Agreement applies to any software library or other
119 program which contains a notice placed by the copyright holder or
120 other authorized party saying it may be distributed under the terms of
121 this Lesser General Public License (also called "this License").
122 Each licensee is addressed as "you".
123
124 A "library" means a collection of software functions and/or data
125 prepared so as to be conveniently linked with application programs
126 (which use some of those functions and data) to form executables.
127
128 The "Library", below, refers to any such software library or work
129 which has been distributed under these terms. A "work based on the
130 Library" means either the Library or any derivative work under
131 copyright law: that is to say, a work containing the Library or a
132 portion of it, either verbatim or with modifications and/or translated
133 straightforwardly into another language. (Hereinafter, translation is
134 included without limitation in the term "modification".)
135
136 "Source code" for a work means the preferred form of the work for
137 making modifications to it. For a library, complete source code means
138 all the source code for all modules it contains, plus any associated
139 interface definition files, plus the scripts used to control compilation
140 and installation of the library.
141
142 Activities other than copying, distribution and modification are not
143 covered by this License; they are outside its scope. The act of
144 running a program using the Library is not restricted, and output from
145 such a program is covered only if its contents constitute a work based
146 on the Library (independent of the use of the Library in a tool for
147 writing it). Whether that is true depends on what the Library does
148 and what the program that uses the Library does.
149
150 1. You may copy and distribute verbatim copies of the Library's
151 complete source code as you receive it, in any medium, provided that
152 you conspicuously and appropriately publish on each copy an
153 appropriate copyright notice and disclaimer of warranty; keep intact
154 all the notices that refer to this License and to the absence of any
155 warranty; and distribute a copy of this License along with the
156 Library.
157
158 You may charge a fee for the physical act of transferring a copy,
159 and you may at your option offer warranty protection in exchange for a
160 fee.
161
162 2. You may modify your copy or copies of the Library or any portion
163 of it, thus forming a work based on the Library, and copy and
164 distribute such modifications or work under the terms of Section 1
165 above, provided that you also meet all of these conditions:
166
167 a) The modified work must itself be a software library.
168
169 b) You must cause the files modified to carry prominent notices
170 stating that you changed the files and the date of any change.
171
172 c) You must cause the whole of the work to be licensed at no
173 charge to all third parties under the terms of this License.
174
175 d) If a facility in the modified Library refers to a function or a
176 table of data to be supplied by an application program that uses
177 the facility, other than as an argument passed when the facility
178 is invoked, then you must make a good faith effort to ensure that,
179 in the event an application does not supply such function or
180 table, the facility still operates, and performs whatever part of
181 its purpose remains meaningful.
182
183 (For example, a function in a library to compute square roots has
184 a purpose that is entirely well-defined independent of the
185 application. Therefore, Subsection 2d requires that any
186 application-supplied function or table used by this function must
187 be optional: if the application does not supply it, the square
188 root function must still compute square roots.)
189
190 These requirements apply to the modified work as a whole. If
191 identifiable sections of that work are not derived from the Library,
192 and can be reasonably considered independent and separate works in
193 themselves, then this License, and its terms, do not apply to those
194 sections when you distribute them as separate works. But when you
195 distribute the same sections as part of a whole which is a work based
196 on the Library, the distribution of the whole must be on the terms of
197 this License, whose permissions for other licensees extend to the
198 entire whole, and thus to each and every part regardless of who wrote
199 it.
200
201 Thus, it is not the intent of this section to claim rights or contest
202 your rights to work written entirely by you; rather, the intent is to
203 exercise the right to control the distribution of derivative or
204 collective works based on the Library.
205
206 In addition, mere aggregation of another work not based on the Library
207 with the Library (or with a work based on the Library) on a volume of
208 a storage or distribution medium does not bring the other work under
209 the scope of this License.
210
211 3. You may opt to apply the terms of the ordinary GNU General Public
212 License instead of this License to a given copy of the Library. To do
213 this, you must alter all the notices that refer to this License, so
214 that they refer to the ordinary GNU General Public License, version 2,
215 instead of to this License. (If a newer version than version 2 of the
216 ordinary GNU General Public License has appeared, then you can specify
217 that version instead if you wish.) Do not make any other change in
218 these notices.
219
220 Once this change is made in a given copy, it is irreversible for
221 that copy, so the ordinary GNU General Public License applies to all
222 subsequent copies and derivative works made from that copy.
223
224 This option is useful when you wish to copy part of the code of
225 the Library into a program that is not a library.
226
227 4. You may copy and distribute the Library (or a portion or
228 derivative of it, under Section 2) in object code or executable form
229 under the terms of Sections 1 and 2 above provided that you accompany
230 it with the complete corresponding machine-readable source code, which
231 must be distributed under the terms of Sections 1 and 2 above on a
232 medium customarily used for software interchange.
233
234 If distribution of object code is made by offering access to copy
235 from a designated place, then offering equivalent access to copy the
236 source code from the same place satisfies the requirement to
237 distribute the source code, even though third parties are not
238 compelled to copy the source along with the object code.
239
240 5. A program that contains no derivative of any portion of the
241 Library, but is designed to work with the Library by being compiled or
242 linked with it, is called a "work that uses the Library". Such a
243 work, in isolation, is not a derivative work of the Library, and
244 therefore falls outside the scope of this License.
245
246 However, linking a "work that uses the Library" with the Library
247 creates an executable that is a derivative of the Library (because it
248 contains portions of the Library), rather than a "work that uses the
249 library". The executable is therefore covered by this License.
250 Section 6 states terms for distribution of such executables.
251
252 When a "work that uses the Library" uses material from a header file
253 that is part of the Library, the object code for the work may be a
254 derivative work of the Library even though the source code is not.
255 Whether this is true is especially significant if the work can be
256 linked without the Library, or if the work is itself a library. The
257 threshold for this to be true is not precisely defined by law.
258
259 If such an object file uses only numerical parameters, data
260 structure layouts and accessors, and small macros and small inline
261 functions (ten lines or less in length), then the use of the object
262 file is unrestricted, regardless of whether it is legally a derivative
263 work. (Executables containing this object code plus portions of the
264 Library will still fall under Section 6.)
265
266 Otherwise, if the work is a derivative of the Library, you may
267 distribute the object code for the work under the terms of Section 6.
268 Any executables containing that work also fall under Section 6,
269 whether or not they are linked directly with the Library itself.
270
271 6. As an exception to the Sections above, you may also combine or
272 link a "work that uses the Library" with the Library to produce a
273 work containing portions of the Library, and distribute that work
274 under terms of your choice, provided that the terms permit
275 modification of the work for the customer's own use and reverse
276 engineering for debugging such modifications.
277
278 You must give prominent notice with each copy of the work that the
279 Library is used in it and that the Library and its use are covered by
280 this License. You must supply a copy of this License. If the work
281 during execution displays copyright notices, you must include the
282 copyright notice for the Library among them, as well as a reference
283 directing the user to the copy of this License. Also, you must do one
284 of these things:
285
286 a) Accompany the work with the complete corresponding
287 machine-readable source code for the Library including whatever
288 changes were used in the work (which must be distributed under
289 Sections 1 and 2 above); and, if the work is an executable linked
290 with the Library, with the complete machine-readable "work that
291 uses the Library", as object code and/or source code, so that the
292 user can modify the Library and then relink to produce a modified
293 executable containing the modified Library. (It is understood
294 that the user who changes the contents of definitions files in the
295 Library will not necessarily be able to recompile the application
296 to use the modified definitions.)
297
298 b) Use a suitable shared library mechanism for linking with the
299 Library. A suitable mechanism is one that (1) uses at run time a
300 copy of the library already present on the user's computer system,
301 rather than copying library functions into the executable, and (2)
302 will operate properly with a modified version of the library, if
303 the user installs one, as long as the modified version is
304 interface-compatible with the version that the work was made with.
305
306 c) Accompany the work with a written offer, valid for at
307 least three years, to give the same user the materials
308 specified in Subsection 6a, above, for a charge no more
309 than the cost of performing this distribution.
310
311 d) If distribution of the work is made by offering access to copy
312 from a designated place, offer equivalent access to copy the above
313 specified materials from the same place.
314
315 e) Verify that the user has already received a copy of these
316 materials or that you have already sent this user a copy.
317
318 For an executable, the required form of the "work that uses the
319 Library" must include any data and utility programs needed for
320 reproducing the executable from it. However, as a special exception,
321 the materials to be distributed need not include anything that is
322 normally distributed (in either source or binary form) with the major
323 components (compiler, kernel, and so on) of the operating system on
324 which the executable runs, unless that component itself accompanies
325 the executable.
326
327 It may happen that this requirement contradicts the license
328 restrictions of other proprietary libraries that do not normally
329 accompany the operating system. Such a contradiction means you cannot
330 use both them and the Library together in an executable that you
331 distribute.
332
333 7. You may place library facilities that are a work based on the
334 Library side-by-side in a single library together with other library
335 facilities not covered by this License, and distribute such a combined
336 library, provided that the separate distribution of the work based on
337 the Library and of the other library facilities is otherwise
338 permitted, and provided that you do these two things:
339
340 a) Accompany the combined library with a copy of the same work
341 based on the Library, uncombined with any other library
342 facilities. This must be distributed under the terms of the
343 Sections above.
344
345 b) Give prominent notice with the combined library of the fact
346 that part of it is a work based on the Library, and explaining
347 where to find the accompanying uncombined form of the same work.
348
349 8. You may not copy, modify, sublicense, link with, or distribute
350 the Library except as expressly provided under this License. Any
351 attempt otherwise to copy, modify, sublicense, link with, or
352 distribute the Library is void, and will automatically terminate your
353 rights under this License. However, parties who have received copies,
354 or rights, from you under this License will not have their licenses
355 terminated so long as such parties remain in full compliance.
356
357 9. You are not required to accept this License, since you have not
358 signed it. However, nothing else grants you permission to modify or
359 distribute the Library or its derivative works. These actions are
360 prohibited by law if you do not accept this License. Therefore, by
361 modifying or distributing the Library (or any work based on the
362 Library), you indicate your acceptance of this License to do so, and
363 all its terms and conditions for copying, distributing or modifying
364 the Library or works based on it.
365
366 10. Each time you redistribute the Library (or any work based on the
367 Library), the recipient automatically receives a license from the
368 original licensor to copy, distribute, link with or modify the Library
369 subject to these terms and conditions. You may not impose any further
370 restrictions on the recipients' exercise of the rights granted herein.
371 You are not responsible for enforcing compliance by third parties with
372 this License.
373
374 11. If, as a consequence of a court judgment or allegation of patent
375 infringement or for any other reason (not limited to patent issues),
376 conditions are imposed on you (whether by court order, agreement or
377 otherwise) that contradict the conditions of this License, they do not
378 excuse you from the conditions of this License. If you cannot
379 distribute so as to satisfy simultaneously your obligations under this
380 License and any other pertinent obligations, then as a consequence you
381 may not distribute the Library at all. For example, if a patent
382 license would not permit royalty-free redistribution of the Library by
383 all those who receive copies directly or indirectly through you, then
384 the only way you could satisfy both it and this License would be to
385 refrain entirely from distribution of the Library.
386
387 If any portion of this section is held invalid or unenforceable under any
388 particular circumstance, the balance of the section is intended to apply,
389 and the section as a whole is intended to apply in other circumstances.
390
391 It is not the purpose of this section to induce you to infringe any
392 patents or other property right claims or to contest validity of any
393 such claims; this section has the sole purpose of protecting the
394 integrity of the free software distribution system which is
395 implemented by public license practices. Many people have made
396 generous contributions to the wide range of software distributed
397 through that system in reliance on consistent application of that
398 system; it is up to the author/donor to decide if he or she is willing
399 to distribute software through any other system and a licensee cannot
400 impose that choice.
401
402 This section is intended to make thoroughly clear what is believed to
403 be a consequence of the rest of this License.
404
405 12. If the distribution and/or use of the Library is restricted in
406 certain countries either by patents or by copyrighted interfaces, the
407 original copyright holder who places the Library under this License may add
408 an explicit geographical distribution limitation excluding those countries,
409 so that distribution is permitted only in or among countries not thus
410 excluded. In such case, this License incorporates the limitation as if
411 written in the body of this License.
412
413 13. The Free Software Foundation may publish revised and/or new
414 versions of the Lesser General Public License from time to time.
415 Such new versions will be similar in spirit to the present version,
416 but may differ in detail to address new problems or concerns.
417
418 Each version is given a distinguishing version number. If the Library
419 specifies a version number of this License which applies to it and
420 "any later version", you have the option of following the terms and
421 conditions either of that version or of any later version published by
422 the Free Software Foundation. If the Library does not specify a
423 license version number, you may choose any version ever published by
424 the Free Software Foundation.
425
426 14. If you wish to incorporate parts of the Library into other free
427 programs whose distribution conditions are incompatible with these,
428 write to the author to ask for permission. For software which is
429 copyrighted by the Free Software Foundation, write to the Free
430 Software Foundation; we sometimes make exceptions for this. Our
431 decision will be guided by the two goals of preserving the free status
432 of all derivatives of our free software and of promoting the sharing
433 and reuse of software generally.
434
435 NO WARRANTY
436
437 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
438 WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
439 EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
440 OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
441 KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
442 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
443 PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
444 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
445 THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
446
447 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
448 WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
449 AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
450 FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
451 CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
452 LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
453 RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
454 FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
455 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
456 DAMAGES.
457
458 END OF TERMS AND CONDITIONS
459
460 How to Apply These Terms to Your New Libraries
461
462 If you develop a new library, and you want it to be of the greatest
463 possible use to the public, we recommend making it free software that
464 everyone can redistribute and change. You can do so by permitting
465 redistribution under these terms (or, alternatively, under the terms of the
466 ordinary General Public License).
467
468 To apply these terms, attach the following notices to the library. It is
469 safest to attach them to the start of each source file to most effectively
470 convey the exclusion of warranty; and each file should have at least the
471 "copyright" line and a pointer to where the full notice is found.
472
473 <one line to give the library's name and a brief idea of what it does.>
474 Copyright (C) <year> <name of author>
475
476 This library is free software; you can redistribute it and/or
477 modify it under the terms of the GNU Lesser General Public
478 License as published by the Free Software Foundation; either
479 version 2.1 of the License, or (at your option) any later version.
480
481 This library is distributed in the hope that it will be useful,
482 but WITHOUT ANY WARRANTY; without even the implied warranty of
483 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
484 Lesser General Public License for more details.
485
486 You should have received a copy of the GNU Lesser General Public
487 License along with this library; if not, write to the Free Software
488 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
489
490 Also add information on how to contact you by electronic and paper mail.
491
492 You should also get your employer (if you work as a programmer) or your
493 school, if any, to sign a "copyright disclaimer" for the library, if
494 necessary. Here is a sample; alter the names:
495
496 Yoyodyne, Inc., hereby disclaims all copyright interest in the
497 library `Frob' (a library for tweaking knobs) written by James Random Hacker.
498
499 <signature of Ty Coon>, 1 April 1990
500 Ty Coon, President of Vice
501
502 That's all there is to it!
503
504
@@ -0,0 +1,12
1 Usage:
2 require 'rubygems'
3 require 'rchardet'
4
5 cd = CharDet.detect(some_data)
6 encoding = cd['encoding']
7 confidence = cd['confidence'] # 0.0 <= confidence <= 1.0
8
9 Project page:
10 http://rubyforge.org/projects/rchardet
11
12 Made for rFeedParser <http://rfeedparser.rubyforge.org>.
@@ -0,0 +1,67
1 ######################## BEGIN LICENSE BLOCK ########################
2 # This library is free software; you can redistribute it and/or
3 # modify it under the terms of the GNU Lesser General Public
4 # License as published by the Free Software Foundation; either
5 # version 2.1 of the License, or (at your option) any later version.
6 #
7 # This library is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 # Lesser General Public License for more details.
11 #
12 # You should have received a copy of the GNU Lesser General Public
13 # License along with this library; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
15 # 02110-1301 USA
16 ######################### END LICENSE BLOCK #########################
17
18 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
19
20 require 'rchardet/charsetprober'
21 require 'rchardet/mbcharsetprober'
22
23 require 'rchardet/big5freq'
24 require 'rchardet/big5prober'
25 require 'rchardet/chardistribution'
26 require 'rchardet/charsetgroupprober'
27
28 require 'rchardet/codingstatemachine'
29 require 'rchardet/constants'
30 require 'rchardet/escprober'
31 require 'rchardet/escsm'
32 require 'rchardet/eucjpprober'
33 require 'rchardet/euckrfreq'
34 require 'rchardet/euckrprober'
35 require 'rchardet/euctwfreq'
36 require 'rchardet/euctwprober'
37 require 'rchardet/gb2312freq'
38 require 'rchardet/gb2312prober'
39 require 'rchardet/hebrewprober'
40 require 'rchardet/jisfreq'
41 require 'rchardet/jpcntx'
42 require 'rchardet/langbulgarianmodel'
43 require 'rchardet/langcyrillicmodel'
44 require 'rchardet/langgreekmodel'
45 require 'rchardet/langhebrewmodel'
46 require 'rchardet/langhungarianmodel'
47 require 'rchardet/langthaimodel'
48 require 'rchardet/latin1prober'
49
50 require 'rchardet/mbcsgroupprober'
51 require 'rchardet/mbcssm'
52 require 'rchardet/sbcharsetprober'
53 require 'rchardet/sbcsgroupprober'
54 require 'rchardet/sjisprober'
55 require 'rchardet/universaldetector'
56 require 'rchardet/utf8prober'
57
58 module CharDet
59 VERSION = "1.3"
60 def CharDet.detect(aBuf)
61 u = UniversalDetector.new
62 u.reset
63 u.feed(aBuf)
64 u.close
65 u.result
66 end
67 end
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -5,6 +5,7
5 /config/database.yml
5 /config/database.yml
6 /config/email.yml
6 /config/email.yml
7 /config/initializers/session_store.rb
7 /config/initializers/session_store.rb
8 /config/initializers/secret_token.rb
8 /coverage
9 /coverage
9 /db/*.db
10 /db/*.db
10 /db/*.sqlite3
11 /db/*.sqlite3
@@ -7,6 +7,7 config/configuration.yml
7 config/database.yml
7 config/database.yml
8 config/email.yml
8 config/email.yml
9 config/initializers/session_store.rb
9 config/initializers/session_store.rb
10 config/initializers/secret_token.rb
10 coverage
11 coverage
11 db/*.db
12 db/*.db
12 db/*.sqlite3
13 db/*.sqlite3
@@ -1,10 +1,12
1 source :rubygems
1 source 'http://rubygems.org'
2
2
3 gem "rails", "2.3.14"
3 gem 'rails', '3.2.3'
4 gem "i18n", "~> 0.4.2"
4 gem 'prototype-rails', '3.2.1'
5 gem "i18n", "~> 0.6.0"
5 gem "coderay", "~> 1.0.6"
6 gem "coderay", "~> 1.0.6"
6 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 gem "fastercsv", "~> 1.5.0", :platforms => [:mri_18, :mingw_18, :jruby]
7 gem "tzinfo", "~> 0.3.31"
8 gem "tzinfo", "~> 0.3.31"
9 gem "builder"
8
10
9 # Optional gem for LDAP authentication
11 # Optional gem for LDAP authentication
10 group :ldap do
12 group :ldap do
@@ -14,6 +16,7 end
14 # Optional gem for OpenID authentication
16 # Optional gem for OpenID authentication
15 group :openid do
17 group :openid do
16 gem "ruby-openid", "~> 2.1.4", :require => "openid"
18 gem "ruby-openid", "~> 2.1.4", :require => "openid"
19 gem "rack-openid"
17 end
20 end
18
21
19 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
22 # Optional gem for exporting the gantt to a PNG file, not supported with jruby
@@ -45,7 +48,7 end
45
48
46 platforms :mri_19, :mingw_19 do
49 platforms :mri_19, :mingw_19 do
47 group :mysql do
50 group :mysql do
48 gem "mysql2", "~> 0.2.7"
51 gem "mysql2", "~> 0.3.11"
49 end
52 end
50 end
53 end
51
54
@@ -69,8 +72,9 group :development do
69 gem "rdoc", ">= 2.4.2"
72 gem "rdoc", ">= 2.4.2"
70 end
73 end
71
74
75
72 group :test do
76 group :test do
73 gem "shoulda", "~> 2.10.3"
77 gem "shoulda"
74 gem "mocha"
78 gem "mocha"
75 end
79 end
76
80
@@ -1,15 +1,7
1 #!/usr/bin/env rake
1 # Add your own tasks in files placed in lib/tasks ending in .rake,
2 # Add your own tasks in files placed in lib/tasks ending in .rake,
2 # for example lib/tasks/switchtower.rake, and they will automatically be available to Rake.
3 # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
4
4 require(File.join(File.dirname(__FILE__), 'config', 'boot'))
5 require File.expand_path('../config/application', __FILE__)
5
6
6 require 'rake'
7 RedmineApp::Application.load_tasks
7 require 'rake/testtask'
8
9 begin
10 require 'rdoc/task'
11 rescue LoadError
12 # RDoc is not available
13 end
14
15 require 'tasks/rails'
@@ -22,9 +22,12 class Unauthorized < Exception; end
22
22
23 class ApplicationController < ActionController::Base
23 class ApplicationController < ActionController::Base
24 include Redmine::I18n
24 include Redmine::I18n
25
26 class_attribute :accept_api_auth_actions
27 class_attribute :accept_rss_auth_actions
28 class_attribute :model_object
25
29
26 layout 'base'
30 layout 'base'
27 exempt_from_layout 'builder', 'rsb'
28
31
29 protect_from_forgery
32 protect_from_forgery
30 def handle_unverified_request
33 def handle_unverified_request
@@ -68,7 +71,6 class ApplicationController < ActionController::Base
68 end
71 end
69
72
70 before_filter :user_setup, :check_if_login_required, :set_localization
73 before_filter :user_setup, :check_if_login_required, :set_localization
71 filter_parameter_logging :password
72
74
73 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
75 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
74 rescue_from ::Unauthorized, :with => :deny_access
76 rescue_from ::Unauthorized, :with => :deny_access
@@ -77,10 +79,6 class ApplicationController < ActionController::Base
77 include Redmine::MenuManager::MenuController
79 include Redmine::MenuManager::MenuController
78 helper Redmine::MenuManager::MenuHelper
80 helper Redmine::MenuManager::MenuHelper
79
81
80 Redmine::Scm::Base.all.each do |scm|
81 require_dependency "repository/#{scm.underscore}"
82 end
83
84 def user_setup
82 def user_setup
85 # Check the settings cache for each request
83 # Check the settings cache for each request
86 Setting.check_cache
84 Setting.check_cache
@@ -242,7 +240,7 class ApplicationController < ActionController::Base
242 end
240 end
243
241
244 def find_model_object
242 def find_model_object
245 model = self.class.read_inheritable_attribute('model_object')
243 model = self.class.model_object
246 if model
244 if model
247 @object = model.find(params[:id])
245 @object = model.find(params[:id])
248 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
246 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
@@ -252,7 +250,7 class ApplicationController < ActionController::Base
252 end
250 end
253
251
254 def self.model_object(model)
252 def self.model_object(model)
255 write_inheritable_attribute('model_object', model)
253 self.model_object = model
256 end
254 end
257
255
258 # Filter for bulk issue operations
256 # Filter for bulk issue operations
@@ -388,9 +386,9 class ApplicationController < ActionController::Base
388
386
389 def self.accept_rss_auth(*actions)
387 def self.accept_rss_auth(*actions)
390 if actions.any?
388 if actions.any?
391 write_inheritable_attribute('accept_rss_auth_actions', actions)
389 self.accept_rss_auth_actions = actions
392 else
390 else
393 read_inheritable_attribute('accept_rss_auth_actions') || []
391 self.accept_rss_auth_actions || []
394 end
392 end
395 end
393 end
396
394
@@ -400,9 +398,9 class ApplicationController < ActionController::Base
400
398
401 def self.accept_api_auth(*actions)
399 def self.accept_api_auth(*actions)
402 if actions.any?
400 if actions.any?
403 write_inheritable_attribute('accept_api_auth_actions', actions)
401 self.accept_api_auth_actions = actions
404 else
402 else
405 read_inheritable_attribute('accept_api_auth_actions') || []
403 self.accept_api_auth_actions || []
406 end
404 end
407 end
405 end
408
406
@@ -523,26 +521,12 class ApplicationController < ActionController::Base
523 else
521 else
524 @error_messages = objects.errors.full_messages
522 @error_messages = objects.errors.full_messages
525 end
523 end
526 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => false
524 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
527 end
528
529 # Overrides #default_template so that the api template
530 # is used automatically if it exists
531 def default_template(action_name = self.action_name)
532 if api_request?
533 begin
534 return self.view_paths.find_template(default_template_name(action_name), 'api')
535 rescue ::ActionView::MissingTemplate
536 # the api template was not found
537 # fallback to the default behaviour
538 end
539 end
540 super
541 end
525 end
542
526
543 # Overrides #pick_layout so that #render with no arguments
527 # Overrides #_include_layout? so that #render with no arguments
544 # doesn't use the layout for api requests
528 # doesn't use the layout for api requests
545 def pick_layout(*args)
529 def _include_layout?(*args)
546 api_request? ? nil : super
530 api_request? ? false : super
547 end
531 end
548 end
532 end
@@ -95,10 +95,11 class MessagesController < ApplicationController
95 # Delete a messages
95 # Delete a messages
96 def destroy
96 def destroy
97 (render_403; return false) unless @message.destroyable_by?(User.current)
97 (render_403; return false) unless @message.destroyable_by?(User.current)
98 r = @message.to_param
98 @message.destroy
99 @message.destroy
99 redirect_to @message.parent.nil? ?
100 redirect_to @message.parent.nil? ?
100 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
101 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
101 { :action => 'show', :id => @message.parent, :r => @message }
102 { :action => 'show', :id => @message.parent, :r => r }
102 end
103 end
103
104
104 def quote
105 def quote
@@ -18,6 +18,7
18 require 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21 require 'redmine/scm/adapters/abstract_adapter'
21
22
22 class ChangesetNotFound < Exception; end
23 class ChangesetNotFound < Exception; end
23 class InvalidRevisionParam < Exception; end
24 class InvalidRevisionParam < Exception; end
@@ -307,8 +308,7 class RepositoriesController < ApplicationController
307 @repository = @project.repository
308 @repository = @project.repository
308 end
309 end
309 (render_404; return false) unless @repository
310 (render_404; return false) unless @repository
310 @path = params[:path].join('/') unless params[:path].nil?
311 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
311 @path ||= ''
312 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
312 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
313 @rev_to = params[:rev_to]
313 @rev_to = params[:rev_to]
314
314
@@ -343,15 +343,15 class RepositoriesController < ApplicationController
343 @date_to = Date.today
343 @date_to = Date.today
344 @date_from = @date_to << 11
344 @date_from = @date_to << 11
345 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
345 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
346 commits_by_day = repository.changesets.count(
346 commits_by_day = Changeset.count(
347 :all, :group => :commit_date,
347 :all, :group => :commit_date,
348 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
348 :conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
349 commits_by_month = [0] * 12
349 commits_by_month = [0] * 12
350 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
350 commits_by_day.each {|c| commits_by_month[c.first.to_date.months_ago] += c.last }
351
351
352 changes_by_day = repository.changes.count(
352 changes_by_day = Change.count(
353 :all, :group => :commit_date,
353 :all, :group => :commit_date, :include => :changeset,
354 :conditions => ["commit_date BETWEEN ? AND ?", @date_from, @date_to])
354 :conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
355 changes_by_month = [0] * 12
355 changes_by_month = [0] * 12
356 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
356 changes_by_day.each {|c| changes_by_month[c.first.to_date.months_ago] += c.last }
357
357
@@ -384,10 +384,10 class RepositoriesController < ApplicationController
384 end
384 end
385
385
386 def graph_commits_per_author(repository)
386 def graph_commits_per_author(repository)
387 commits_by_author = repository.changesets.count(:all, :group => :committer)
387 commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
388 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
388 commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
389
389
390 changes_by_author = repository.changes.count(:all, :group => :committer)
390 changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
391 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
391 h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
392
392
393 fields = commits_by_author.collect {|r| r.first}
393 fields = commits_by_author.collect {|r| r.first}
@@ -109,7 +109,7 private
109 @watched = klass.find(params[:object_id])
109 @watched = klass.find(params[:object_id])
110 @project = @watched.project
110 @project = @watched.project
111 elsif params[:project_id]
111 elsif params[:project_id]
112 @project = Project.visible.find(params[:project_id])
112 @project = Project.visible.find_by_param(params[:project_id])
113 end
113 end
114 rescue
114 rescue
115 render_404
115 render_404
@@ -163,6 +163,8 class WikiController < ApplicationController
163 # Optimistic locking exception
163 # Optimistic locking exception
164 flash.now[:error] = l(:notice_locking_conflict)
164 flash.now[:error] = l(:notice_locking_conflict)
165 render :action => 'edit'
165 render :action => 'edit'
166 rescue ActiveRecord::RecordNotSaved
167 render :action => 'edit'
166 end
168 end
167
169
168 # rename a page
170 # rename a page
@@ -930,6 +930,9 module ApplicationHelper
930 def labelled_form_for(*args, &proc)
930 def labelled_form_for(*args, &proc)
931 args << {} unless args.last.is_a?(Hash)
931 args << {} unless args.last.is_a?(Hash)
932 options = args.last
932 options = args.last
933 if args.first.is_a?(Symbol)
934 options.merge!(:as => args.shift)
935 end
933 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
934 form_for(*args, &proc)
937 form_for(*args, &proc)
935 end
938 end
@@ -1060,7 +1063,7 module ApplicationHelper
1060 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1063 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1061 def avatar(user, options = { })
1064 def avatar(user, options = { })
1062 if Setting.gravatar_enabled?
1065 if Setting.gravatar_enabled?
1063 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1066 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1064 email = nil
1067 email = nil
1065 if user.respond_to?(:mail)
1068 if user.respond_to?(:mail)
1066 email = user.mail
1069 email = user.mail
@@ -1079,7 +1082,7 module ApplicationHelper
1079
1082
1080 # Returns the javascript tags that are included in the html layout head
1083 # Returns the javascript tags that are included in the html layout head
1081 def javascript_heads
1084 def javascript_heads
1082 tags = javascript_include_tag(:defaults)
1085 tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
1083 unless User.current.pref.warn_on_leaving_unsaved == '0'
1086 unless User.current.pref.warn_on_leaving_unsaved == '0'
1084 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1087 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1085 end
1088 end
@@ -21,14 +21,14 module WikiHelper
21
21
22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
22 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
23 pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
24 s = ''
24 s = ''.html_safe
25 if pages.has_key?(parent)
25 if pages.has_key?(parent)
26 pages[parent].each do |page|
26 pages[parent].each do |page|
27 attrs = "value='#{page.id}'"
27 attrs = "value='#{page.id}'"
28 attrs << " selected='selected'" if selected == page
28 attrs << " selected='selected'" if selected == page
29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
29 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : ''
30
30
31 s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
31 s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
32 wiki_page_options_for_select(pages, selected, page, level + 1)
32 wiki_page_options_for_select(pages, selected, page, level + 1)
33 end
33 end
34 end
34 end
@@ -80,7 +80,7 class CustomField < ActiveRecord::Base
80 when 'bool'
80 when 'bool'
81 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
81 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
82 else
82 else
83 read_possible_values_utf8_encoded || []
83 possible_values || []
84 end
84 end
85 end
85 end
86
86
@@ -91,14 +91,20 class CustomField < ActiveRecord::Base
91 when 'bool'
91 when 'bool'
92 ['1', '0']
92 ['1', '0']
93 else
93 else
94 read_possible_values_utf8_encoded
94 values = super()
95 if values.is_a?(Array)
96 values.each do |value|
97 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
98 end
99 end
100 values
95 end
101 end
96 end
102 end
97
103
98 # Makes possible_values accept a multiline string
104 # Makes possible_values accept a multiline string
99 def possible_values=(arg)
105 def possible_values=(arg)
100 if arg.is_a?(Array)
106 if arg.is_a?(Array)
101 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
107 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
102 else
108 else
103 self.possible_values = arg.to_s.split(/[\n\r]+/)
109 self.possible_values = arg.to_s.split(/[\n\r]+/)
104 end
110 end
@@ -218,14 +224,4 class CustomField < ActiveRecord::Base
218 end
224 end
219 errs
225 errs
220 end
226 end
221
222 def read_possible_values_utf8_encoded
223 values = read_attribute(:possible_values)
224 if values.is_a?(Array)
225 values.each do |value|
226 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
227 end
228 end
229 values
230 end
231 end
227 end
@@ -246,8 +246,8 class Issue < ActiveRecord::Base
246 write_attribute(:description, arg)
246 write_attribute(:description, arg)
247 end
247 end
248
248
249 # Overrides attributes= so that project and tracker get assigned first
249 # Overrides assign_attributes so that project and tracker get assigned first
250 def attributes_with_project_and_tracker_first=(new_attributes, *args)
250 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
251 return if new_attributes.nil?
251 return if new_attributes.nil?
252 attrs = new_attributes.dup
252 attrs = new_attributes.dup
253 attrs.stringify_keys!
253 attrs.stringify_keys!
@@ -257,10 +257,10 class Issue < ActiveRecord::Base
257 send "#{attr}=", attrs.delete(attr)
257 send "#{attr}=", attrs.delete(attr)
258 end
258 end
259 end
259 end
260 send :attributes_without_project_and_tracker_first=, attrs, *args
260 send :assign_attributes_without_project_and_tracker_first, attrs, *args
261 end
261 end
262 # Do not redefine alias chain on reload (see #4838)
262 # Do not redefine alias chain on reload (see #4838)
263 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
263 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
264
264
265 def estimated_hours=(h)
265 def estimated_hours=(h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
266 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
@@ -350,7 +350,7 class Issue < ActiveRecord::Base
350 end
350 end
351
351
352 # mass-assignment security bypass
352 # mass-assignment security bypass
353 self.send :attributes=, attrs, false
353 assign_attributes attrs, :without_protection => true
354 end
354 end
355
355
356 def done_ratio
356 def done_ratio
@@ -921,7 +921,7 class Issue < ActiveRecord::Base
921 p.estimated_hours = nil if p.estimated_hours == 0.0
921 p.estimated_hours = nil if p.estimated_hours == 0.0
922
922
923 # ancestors will be recursively updated
923 # ancestors will be recursively updated
924 p.save(false)
924 p.save(:validate => false)
925 end
925 end
926 end
926 end
927
927
@@ -15,7 +15,9
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 require 'vendor/tmail'
19
20 class MailHandler
19 include ActionView::Helpers::SanitizeHelper
21 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
22 include Redmine::I18n
21
23
@@ -39,7 +41,14 class MailHandler < ActionMailer::Base
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
41 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
42
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42 super email
44
45 mail = TMail::Mail.parse(email)
46 mail.base64_decode
47 new.receive(mail)
48 end
49
50 def logger
51 Rails.logger
43 end
52 end
44
53
45 cattr_accessor :ignored_emails_headers
54 cattr_accessor :ignored_emails_headers
@@ -21,13 +21,10 class Mailer < ActionMailer::Base
21 helper :issues
21 helper :issues
22 helper :custom_fields
22 helper :custom_fields
23
23
24 include ActionController::UrlWriter
25 include Redmine::I18n
24 include Redmine::I18n
26
25
27 def self.default_url_options
26 def self.default_url_options
28 h = Setting.host_name
27 { :host => Setting.host_name, :protocol => Setting.protocol }
29 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
30 { :host => h, :protocol => Setting.protocol }
31 end
28 end
32
29
33 # Builds a tmail object used to email recipients of the added issue.
30 # Builds a tmail object used to email recipients of the added issue.
@@ -42,12 +39,13 class Mailer < ActionMailer::Base
42 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
39 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
43 message_id issue
40 message_id issue
44 @author = issue.author
41 @author = issue.author
45 recipients issue.recipients
42 @issue = issue
46 cc(issue.watcher_recipients - @recipients)
43 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
47 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
44 recipients = issue.recipients
48 body :issue => issue,
45 cc = issue.watcher_recipients - recipients
49 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
46 mail :to => recipients,
50 render_multipart('issue_add', body)
47 :cc => cc,
48 :subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
51 end
49 end
52
50
53 # Builds a tmail object used to email recipients of the edited issue.
51 # Builds a tmail object used to email recipients of the edited issue.
@@ -64,30 +62,29 class Mailer < ActionMailer::Base
64 message_id journal
62 message_id journal
65 references issue
63 references issue
66 @author = journal.user
64 @author = journal.user
67 recipients issue.recipients
65 recipients = issue.recipients
68 # Watchers in cc
66 # Watchers in cc
69 cc(issue.watcher_recipients - @recipients)
67 cc = issue.watcher_recipients - recipients
70 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
68 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
71 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
69 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
72 s << issue.subject
70 s << issue.subject
73 subject s
71 @issue = issue
74 body :issue => issue,
72 @journal = journal
75 :journal => journal,
73 @issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
76 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
74 mail :to => recipients,
77
75 :cc => cc,
78 render_multipart('issue_edit', body)
76 :subject => s
79 end
77 end
80
78
81 def reminder(user, issues, days)
79 def reminder(user, issues, days)
82 set_language_if_valid user.language
80 set_language_if_valid user.language
83 recipients user.mail
81 @issues = issues
84 subject l(:mail_subject_reminder, :count => issues.size, :days => days)
82 @days = days
85 body :issues => issues,
83 @issues_url = url_for(:controller => 'issues', :action => 'index',
86 :days => days,
87 :issues_url => url_for(:controller => 'issues', :action => 'index',
88 :set_filter => 1, :assigned_to_id => user.id,
84 :set_filter => 1, :assigned_to_id => user.id,
89 :sort => 'due_date:asc')
85 :sort => 'due_date:asc')
90 render_multipart('reminder', body)
86 mail :to => user.mail,
87 :subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
91 end
88 end
92
89
93 # Builds a tmail object used to email users belonging to the added document's project.
90 # Builds a tmail object used to email users belonging to the added document's project.
@@ -97,12 +94,11 class Mailer < ActionMailer::Base
97 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
94 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
98 def document_added(document)
95 def document_added(document)
99 redmine_headers 'Project' => document.project.identifier
96 redmine_headers 'Project' => document.project.identifier
100 recipients document.recipients
101 @author = User.current
97 @author = User.current
102 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
98 @document = document
103 body :document => document,
99 @document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
104 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
100 mail :to => document.recipients,
105 render_multipart('document_added', body)
101 :subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
106 end
102 end
107
103
108 # Builds a tmail object used to email recipients of a project when an attachements are added.
104 # Builds a tmail object used to email recipients of a project when an attachements are added.
@@ -119,22 +115,22 class Mailer < ActionMailer::Base
119 when 'Project'
115 when 'Project'
120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
116 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
121 added_to = "#{l(:label_project)}: #{container}"
117 added_to = "#{l(:label_project)}: #{container}"
122 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
118 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
123 when 'Version'
119 when 'Version'
124 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
120 added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
125 added_to = "#{l(:label_version)}: #{container.name}"
121 added_to = "#{l(:label_version)}: #{container.name}"
126 recipients container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
122 recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
127 when 'Document'
123 when 'Document'
128 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
124 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
129 added_to = "#{l(:label_document)}: #{container.title}"
125 added_to = "#{l(:label_document)}: #{container.title}"
130 recipients container.recipients
126 recipients = container.recipients
131 end
127 end
132 redmine_headers 'Project' => container.project.identifier
128 redmine_headers 'Project' => container.project.identifier
133 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
129 @attachments = attachments
134 body :attachments => attachments,
130 @added_to = added_to
135 :added_to => added_to,
131 @added_to_url = added_to_url
136 :added_to_url => added_to_url
132 mail :to => recipients,
137 render_multipart('attachments_added', body)
133 :subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
138 end
134 end
139
135
140 # Builds a tmail object used to email recipients of a news' project when a news item is added.
136 # Builds a tmail object used to email recipients of a news' project when a news item is added.
@@ -146,11 +142,10 class Mailer < ActionMailer::Base
146 redmine_headers 'Project' => news.project.identifier
142 redmine_headers 'Project' => news.project.identifier
147 @author = news.author
143 @author = news.author
148 message_id news
144 message_id news
149 recipients news.recipients
145 @news = news
150 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
146 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
151 body :news => news,
147 mail :to => news.recipients,
152 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
148 :subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
153 render_multipart('news_added', body)
154 end
149 end
155
150
156 # Builds a tmail object used to email recipients of a news' project when a news comment is added.
151 # Builds a tmail object used to email recipients of a news' project when a news comment is added.
@@ -163,13 +158,12 class Mailer < ActionMailer::Base
163 redmine_headers 'Project' => news.project.identifier
158 redmine_headers 'Project' => news.project.identifier
164 @author = comment.author
159 @author = comment.author
165 message_id comment
160 message_id comment
166 recipients news.recipients
161 @news = news
167 cc news.watcher_recipients
162 @comment = comment
168 subject "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
163 @news_url = url_for(:controller => 'news', :action => 'show', :id => news)
169 body :news => news,
164 mail :to => news.recipients,
170 :comment => comment,
165 :cc => news.watcher_recipients,
171 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
166 :subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
172 render_multipart('news_comment_added', body)
173 end
167 end
174
168
175 # Builds a tmail object used to email the recipients of the specified message that was posted.
169 # Builds a tmail object used to email the recipients of the specified message that was posted.
@@ -183,12 +177,13 class Mailer < ActionMailer::Base
183 @author = message.author
177 @author = message.author
184 message_id message
178 message_id message
185 references message.parent unless message.parent.nil?
179 references message.parent unless message.parent.nil?
186 recipients(message.recipients)
180 recipients = message.recipients
187 cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
181 cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
188 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
182 @message = message
189 body :message => message,
183 @message_url = url_for(message.event_url)
190 :message_url => url_for(message.event_url)
184 mail :to => recipients,
191 render_multipart('message_posted', body)
185 :cc => cc,
186 :subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
192 end
187 end
193
188
194 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
189 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
@@ -201,14 +196,15 class Mailer < ActionMailer::Base
201 'Wiki-Page-Id' => wiki_content.page.id
196 'Wiki-Page-Id' => wiki_content.page.id
202 @author = wiki_content.author
197 @author = wiki_content.author
203 message_id wiki_content
198 message_id wiki_content
204 recipients wiki_content.recipients
199 recipients = wiki_content.recipients
205 cc(wiki_content.page.wiki.watcher_recipients - recipients)
200 cc = wiki_content.page.wiki.watcher_recipients - recipients
206 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
201 @wiki_content = wiki_content
207 body :wiki_content => wiki_content,
202 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
208 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
209 :project_id => wiki_content.project,
203 :project_id => wiki_content.project,
210 :id => wiki_content.page.title)
204 :id => wiki_content.page.title)
211 render_multipart('wiki_content_added', body)
205 mail :to => recipients,
206 :cc => cc,
207 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
212 end
208 end
213
209
214 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
210 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
@@ -221,17 +217,18 class Mailer < ActionMailer::Base
221 'Wiki-Page-Id' => wiki_content.page.id
217 'Wiki-Page-Id' => wiki_content.page.id
222 @author = wiki_content.author
218 @author = wiki_content.author
223 message_id wiki_content
219 message_id wiki_content
224 recipients wiki_content.recipients
220 recipients = wiki_content.recipients
225 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
221 cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
226 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
222 @wiki_content = wiki_content
227 body :wiki_content => wiki_content,
223 @wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
228 :wiki_content_url => url_for(:controller => 'wiki', :action => 'show',
229 :project_id => wiki_content.project,
224 :project_id => wiki_content.project,
230 :id => wiki_content.page.title),
225 :id => wiki_content.page.title)
231 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff',
226 @wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
232 :project_id => wiki_content.project, :id => wiki_content.page.title,
227 :project_id => wiki_content.project, :id => wiki_content.page.title,
233 :version => wiki_content.version)
228 :version => wiki_content.version)
234 render_multipart('wiki_content_updated', body)
229 mail :to => recipients,
230 :cc => cc,
231 :subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
235 end
232 end
236
233
237 # Builds a tmail object used to email the specified user their account information.
234 # Builds a tmail object used to email the specified user their account information.
@@ -241,12 +238,11 class Mailer < ActionMailer::Base
241 # Mailer.deliver_account_information(user, password) => sends account information to the user
238 # Mailer.deliver_account_information(user, password) => sends account information to the user
242 def account_information(user, password)
239 def account_information(user, password)
243 set_language_if_valid user.language
240 set_language_if_valid user.language
244 recipients user.mail
241 @user = user
245 subject l(:mail_subject_register, Setting.app_title)
242 @password = password
246 body :user => user,
243 @login_url = url_for(:controller => 'account', :action => 'login')
247 :password => password,
244 mail :to => user.mail,
248 :login_url => url_for(:controller => 'account', :action => 'login')
245 :subject => l(:mail_subject_register, Setting.app_title)
249 render_multipart('account_information', body)
250 end
246 end
251
247
252 # Builds a tmail object used to email all active administrators of an account activation request.
248 # Builds a tmail object used to email all active administrators of an account activation request.
@@ -256,13 +252,13 class Mailer < ActionMailer::Base
256 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
252 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
257 def account_activation_request(user)
253 def account_activation_request(user)
258 # Send the email to all active administrators
254 # Send the email to all active administrators
259 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
255 recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
260 subject l(:mail_subject_account_activation_request, Setting.app_title)
256 @user = user
261 body :user => user,
257 @url = url_for(:controller => 'users', :action => 'index',
262 :url => url_for(:controller => 'users', :action => 'index',
263 :status => User::STATUS_REGISTERED,
258 :status => User::STATUS_REGISTERED,
264 :sort_key => 'created_on', :sort_order => 'desc')
259 :sort_key => 'created_on', :sort_order => 'desc')
265 render_multipart('account_activation_request', body)
260 mail :to => recipients,
261 :subject => l(:mail_subject_account_activation_request, Setting.app_title)
266 end
262 end
267
263
268 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
264 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
@@ -272,37 +268,33 class Mailer < ActionMailer::Base
272 # Mailer.deliver_account_activated(user) => sends an email to the registered user
268 # Mailer.deliver_account_activated(user) => sends an email to the registered user
273 def account_activated(user)
269 def account_activated(user)
274 set_language_if_valid user.language
270 set_language_if_valid user.language
275 recipients user.mail
271 @user = user
276 subject l(:mail_subject_register, Setting.app_title)
272 @login_url = url_for(:controller => 'account', :action => 'login')
277 body :user => user,
273 mail :to => user.mail,
278 :login_url => url_for(:controller => 'account', :action => 'login')
274 :subject => l(:mail_subject_register, Setting.app_title)
279 render_multipart('account_activated', body)
280 end
275 end
281
276
282 def lost_password(token)
277 def lost_password(token)
283 set_language_if_valid(token.user.language)
278 set_language_if_valid(token.user.language)
284 recipients token.user.mail
279 @token = token
285 subject l(:mail_subject_lost_password, Setting.app_title)
280 @url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
286 body :token => token,
281 mail :to => token.user.mail,
287 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
282 :subject => l(:mail_subject_lost_password, Setting.app_title)
288 render_multipart('lost_password', body)
289 end
283 end
290
284
291 def register(token)
285 def register(token)
292 set_language_if_valid(token.user.language)
286 set_language_if_valid(token.user.language)
293 recipients token.user.mail
287 @token = token
294 subject l(:mail_subject_register, Setting.app_title)
288 @url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
295 body :token => token,
289 mail :to => token.user.mail,
296 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
290 :subject => l(:mail_subject_register, Setting.app_title)
297 render_multipart('register', body)
298 end
291 end
299
292
300 def test_email(user)
293 def test_email(user)
301 set_language_if_valid(user.language)
294 set_language_if_valid(user.language)
302 recipients user.mail
295 @url = url_for(:controller => 'welcome')
303 subject 'Redmine test'
296 mail :to => user.mail,
304 body :url => url_for(:controller => 'welcome')
297 :subject => 'Redmine test'
305 render_multipart('test_email', body)
306 end
298 end
307
299
308 # Overrides default deliver! method to prevent from sending an email
300 # Overrides default deliver! method to prevent from sending an email
@@ -313,13 +305,6 class Mailer < ActionMailer::Base
313 (cc.nil? || cc.empty?) &&
305 (cc.nil? || cc.empty?) &&
314 (bcc.nil? || bcc.empty?)
306 (bcc.nil? || bcc.empty?)
315
307
316 # Set Message-Id and References
317 if @message_id_object
318 mail.message_id = self.class.message_id_for(@message_id_object)
319 end
320 if @references_objects
321 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
322 end
323
308
324 # Log errors when raise_delivery_errors is set to false, Rails does not
309 # Log errors when raise_delivery_errors is set to false, Rails does not
325 raise_errors = self.class.raise_delivery_errors
310 raise_errors = self.class.raise_delivery_errors
@@ -383,83 +368,72 class Mailer < ActionMailer::Base
383 ActionMailer::Base.delivery_method = saved_method
368 ActionMailer::Base.delivery_method = saved_method
384 end
369 end
385
370
386 private
371 def mail(headers={})
387 def initialize_defaults(method_name)
372 headers.merge! 'X-Mailer' => 'Redmine',
388 super
389 @initial_language = current_language
390 set_language_if_valid Setting.default_language
391 from Setting.mail_from
392
393 # Common headers
394 headers 'X-Mailer' => 'Redmine',
395 'X-Redmine-Host' => Setting.host_name,
373 'X-Redmine-Host' => Setting.host_name,
396 'X-Redmine-Site' => Setting.app_title,
374 'X-Redmine-Site' => Setting.app_title,
397 'X-Auto-Response-Suppress' => 'OOF',
375 'X-Auto-Response-Suppress' => 'OOF',
398 'Auto-Submitted' => 'auto-generated'
376 'Auto-Submitted' => 'auto-generated',
399 end
377 'From' => Setting.mail_from
400
378
401 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
402 def redmine_headers(h)
403 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
404 end
405
406 # Overrides the create_mail method
407 def create_mail
408 # Removes the author from the recipients and cc
379 # Removes the author from the recipients and cc
409 # if he doesn't want to receive notifications about what he does
380 # if he doesn't want to receive notifications about what he does
410 if @author && @author.logged? && @author.pref[:no_self_notified]
381 if @author && @author.logged? && @author.pref[:no_self_notified]
411 if recipients
382 headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
412 recipients((recipients.is_a?(Array) ? recipients : [recipients]) - [@author.mail])
383 headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
413 end
414 if cc
415 cc((cc.is_a?(Array) ? cc : [cc]) - [@author.mail])
416 end
417 end
384 end
418
385
419 if @author && @author.logged?
386 if @author && @author.logged?
420 redmine_headers 'Sender' => @author.login
387 redmine_headers 'Sender' => @author.login
421 end
388 end
422
389
423 notified_users = [recipients, cc].flatten.compact.uniq
424 # Rails would log recipients only, not cc and bcc
425 mylogger.info "Sending email notification to: #{notified_users.join(', ')}" if mylogger
426
427 # Blind carbon copy recipients
390 # Blind carbon copy recipients
428 if Setting.bcc_recipients?
391 if Setting.bcc_recipients?
429 bcc(notified_users)
392 headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
430 recipients []
393 headers[:to] = nil
431 cc []
394 headers[:cc] = nil
395 end
396
397 if @message_id_object
398 headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
399 end
400 if @references_objects
401 headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
402 end
403
404 super headers do |format|
405 format.text
406 format.html unless Setting.plain_text_mail?
432 end
407 end
408
409 set_language_if_valid @initial_language
410 end
411
412 def initialize(*args)
413 @initial_language = current_language
414 set_language_if_valid Setting.default_language
415 super
416 end
417
418 def self.deliver_mail(mail)
419 return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
433 super
420 super
434 end
421 end
435
422
436 # Rails 2.3 has problems rendering implicit multipart messages with
423 def self.method_missing(method, *args, &block)
437 # layouts so this method will wrap an multipart messages with
424 if m = method.to_s.match(%r{^deliver_(.+)$})
438 # explicit parts.
425 send(m[1], *args).deliver
439 #
440 # https://rails.lighthouseapp.com/projects/8994/tickets/2338-actionmailer-mailer-views-and-content-type
441 # https://rails.lighthouseapp.com/projects/8994/tickets/1799-actionmailer-doesnt-set-template_format-when-rendering-layouts
442
443 def render_multipart(method_name, body)
444 if Setting.plain_text_mail?
445 content_type "text/plain"
446 body render(:file => "#{method_name}.text.erb",
447 :body => body,
448 :layout => 'mailer.text.erb')
449 else
426 else
450 content_type "multipart/alternative"
427 super
451 part :content_type => "text/plain",
452 :body => render(:file => "#{method_name}.text.erb",
453 :body => body, :layout => 'mailer.text.erb')
454 part :content_type => "text/html",
455 :body => render_message("#{method_name}.html.erb", body)
456 end
428 end
457 end
429 end
458
430
459 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
431 private
460 def self.controller_path
432
461 ''
433 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
462 end unless respond_to?('controller_path')
434 def redmine_headers(h)
435 h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
436 end
463
437
464 # Returns a predictable Message-Id for the given object
438 # Returns a predictable Message-Id for the given object
465 def self.message_id_for(object)
439 def self.message_id_for(object)
@@ -469,11 +443,9 class Mailer < ActionMailer::Base
469 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
443 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
470 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
444 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
471 host = "#{::Socket.gethostname}.redmine" if host.empty?
445 host = "#{::Socket.gethostname}.redmine" if host.empty?
472 "<#{hash}@#{host}>"
446 "#{hash}@#{host}"
473 end
447 end
474
448
475 private
476
477 def message_id(object)
449 def message_id(object)
478 @message_id_object = object
450 @message_id_object = object
479 end
451 end
@@ -487,12 +459,3 class Mailer < ActionMailer::Base
487 Rails.logger
459 Rails.logger
488 end
460 end
489 end
461 end
490
491 # Patch TMail so that message_id is not overwritten
492 module TMail
493 class Mail
494 def add_message_id( fqdn = nil )
495 self.message_id ||= ::TMail::new_message_id(fqdn)
496 end
497 end
498 end
@@ -272,6 +272,10 class Project < ActiveRecord::Base
272 end
272 end
273 end
273 end
274
274
275 def self.find_by_param(*args)
276 self.find(*args)
277 end
278
275 def reload(*args)
279 def reload(*args)
276 @shared_versions = nil
280 @shared_versions = nil
277 @rolled_up_versions = nil
281 @rolled_up_versions = nil
@@ -57,7 +57,7 class Repository < ActiveRecord::Base
57 end
57 end
58
58
59 alias :attributes_without_extra_info= :attributes=
59 alias :attributes_without_extra_info= :attributes=
60 def attributes=(new_attributes, guard_protected_attributes = true)
60 def attributes=(new_attributes)
61 return if new_attributes.nil?
61 return if new_attributes.nil?
62 attributes = new_attributes.dup
62 attributes = new_attributes.dup
63 attributes.stringify_keys!
63 attributes.stringify_keys!
@@ -72,7 +72,7 class Repository < ActiveRecord::Base
72 end
72 end
73 end
73 end
74
74
75 send :attributes_without_extra_info=, p, guard_protected_attributes
75 send :attributes_without_extra_info=, p
76 if p_extra.keys.any?
76 if p_extra.keys.any?
77 merge_extra_info(p_extra)
77 merge_extra_info(p_extra)
78 end
78 end
@@ -36,7 +36,7 class Role < ActiveRecord::Base
36 before_destroy :check_deletable
36 before_destroy :check_deletable
37 has_many :workflows, :dependent => :delete_all do
37 has_many :workflows, :dependent => :delete_all do
38 def copy(source_role)
38 def copy(source_role)
39 Workflow.copy(nil, source_role, nil, proxy_owner)
39 Workflow.copy(nil, source_role, nil, proxy_association.owner)
40 end
40 end
41 end
41 end
42
42
@@ -20,7 +20,7 class Tracker < ActiveRecord::Base
20 has_many :issues
20 has_many :issues
21 has_many :workflows, :dependent => :delete_all do
21 has_many :workflows, :dependent => :delete_all do
22 def copy(source_tracker)
22 def copy(source_tracker)
23 Workflow.copy(source_tracker, nil, proxy_owner, nil)
23 Workflow.copy(source_tracker, nil, proxy_association.owner, nil)
24 end
24 end
25 end
25 end
26
26
@@ -1,6 +1,6
1 <%= call_hook :view_account_login_top %>
1 <%= call_hook :view_account_login_top %>
2 <div id="login-form">
2 <div id="login-form">
3 <% form_tag({:action=> "login"}) do %>
3 <%= form_tag({:action=> "login"}) do %>
4 <%= back_url_hidden_field_tag %>
4 <%= back_url_hidden_field_tag %>
5 <table>
5 <table>
6 <tr>
6 <tr>
@@ -1,7 +1,7
1 <h2><%=l(:label_password_lost)%></h2>
1 <h2><%=l(:label_password_lost)%></h2>
2
2
3 <div class="box">
3 <div class="box">
4 <% form_tag({:action=> "lost_password"}, :class => "tabular") do %>
4 <%= form_tag({:action=> "lost_password"}, :class => "tabular") do %>
5
5
6 <p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
6 <p><label for="mail"><%=l(:field_mail)%> <span class="required">*</span></label>
7 <%= text_field_tag 'mail', nil, :size => 40 %>
7 <%= text_field_tag 'mail', nil, :size => 40 %>
@@ -2,7 +2,7
2
2
3 <%= error_messages_for 'user' %>
3 <%= error_messages_for 'user' %>
4
4
5 <% form_tag({:token => @token.value}) do %>
5 <%= form_tag({:token => @token.value}) do %>
6 <div class="box tabular">
6 <div class="box tabular">
7 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
7 <p><label for="new_password"><%=l(:field_new_password)%> <span class="required">*</span></label>
8 <%= password_field_tag 'new_password', nil, :size => 25 %>
8 <%= password_field_tag 'new_password', nil, :size => 25 %>
@@ -1,6 +1,6
1 <h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
1 <h2><%=l(:label_register)%> <%=link_to l(:label_login_with_open_id_option), signin_url if Setting.openid? %></h2>
2
2
3 <% labelled_form_for @user, :url => {:action => 'register'} do |f| %>
3 <%= labelled_form_for @user, :url => {:action => 'register'} do |f| %>
4 <%= error_messages_for 'user' %>
4 <%= error_messages_for 'user' %>
5
5
6 <div class="box tabular">
6 <div class="box tabular">
@@ -40,7 +40,7
40 <% end %>
40 <% end %>
41
41
42 <% content_for :sidebar do %>
42 <% content_for :sidebar do %>
43 <% form_tag({}, :method => :get) do %>
43 <%= form_tag({}, :method => :get) do %>
44 <h3><%= l(:label_activity) %></h3>
44 <h3><%= l(:label_activity) %></h3>
45 <p><% @activity.event_types.each do |t| %>
45 <p><% @activity.event_types.each do |t| %>
46 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
46 <%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %>
@@ -1,5 +1,5
1 <div class="nodata">
1 <div class="nodata">
2 <% form_tag({:action => 'default_configuration'}) do %>
2 <%= form_tag({:action => 'default_configuration'}) do %>
3 <%= simple_format(l(:text_no_configuration_data)) %>
3 <%= simple_format(l(:text_no_configuration_data)) %>
4 <p><%= l(:field_language) %>:
4 <p><%= l(:field_language) %>:
5 <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
5 <%= select_tag 'lang', options_for_select(lang_options_for_select(false), current_language.to_s) %>
@@ -4,7 +4,7
4
4
5 <h2><%=l(:label_project_plural)%></h2>
5 <h2><%=l(:label_project_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label for='status'><%= l(:field_status) %> :</label>
9 <label for='status'><%= l(:field_status) %> :</label>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', project_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
@@ -7,7 +7,7
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
7 <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
8 </div>
8 </div>
9 <p>
9 <p>
10 <% form_tag({}, :method => 'get') do %>
10 <%= form_tag({}, :method => 'get') do %>
11 <label><%= l(:label_view_diff) %></label>
11 <label><%= l(:label_view_diff) %></label>
12 <%= select_tag 'type',
12 <%= select_tag 'type',
13 options_for_select(
13 options_for_select(
@@ -1,6 +1,6
1 <h2><%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
1 <h2><%=l(:label_auth_source)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
2
2
3 <% form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %>
3 <%= form_tag({:action => 'update', :id => @auth_source}, :method => :put, :class => "tabular") do %>
4 <%= render :partial => auth_source_partial_name(@auth_source) %>
4 <%= render :partial => auth_source_partial_name(@auth_source) %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%=l(:label_auth_source_new)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
1 <h2><%=l(:label_auth_source_new)%> (<%= h(@auth_source.auth_method_name) %>)</h2>
2
2
3 <% form_tag({:action => 'create'}, :class => "tabular") do %>
3 <%= form_tag({:action => 'create'}, :class => "tabular") do %>
4 <%= hidden_field_tag 'type', @auth_source.type %>
4 <%= hidden_field_tag 'type', @auth_source.type %>
5 <%= render :partial => auth_source_partial_name(@auth_source) %>
5 <%= render :partial => auth_source_partial_name(@auth_source) %>
6 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create) %>
@@ -1,6 +1,6
1 <h2><%= l(:label_board) %></h2>
1 <h2><%= l(:label_board) %></h2>
2
2
3 <% labelled_form_for @board, :url => project_board_path(@project, @board) do |f| %>
3 <%= labelled_form_for @board, :url => project_board_path(@project, @board) do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= l(:label_board_new) %></h2>
1 <h2><%= l(:label_board_new) %></h2>
2
2
3 <% labelled_form_for @board, :url => project_boards_path(@project) do |f| %>
3 <%= labelled_form_for @board, :url => project_boards_path(@project) do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -11,7 +11,7
11 <div id="add-message" style="display:none;">
11 <div id="add-message" style="display:none;">
12 <% if authorize_for('messages', 'new') %>
12 <% if authorize_for('messages', 'new') %>
13 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
13 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
14 <% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
14 <%= form_for @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
15 <%= render :partial => 'messages/form', :locals => {:f => f} %>
15 <%= render :partial => 'messages/form', :locals => {:f => f} %>
16 <p><%= submit_tag l(:button_create) %>
16 <p><%= submit_tag l(:button_create) %>
17 <%= link_to_remote l(:label_preview),
17 <%= link_to_remote l(:label_preview),
@@ -1,6 +1,7
1 <h2><%= @query.new_record? ? l(:label_calendar) : h(@query.name) %></h2>
1 <h2><%= @query.new_record? ? l(:label_calendar) : h(@query.name) %></h2>
2
2
3 <% form_tag({:controller => 'calendars', :action => 'show', :project_id => @project}, :method => :get, :id => 'query_form') do %>
3 <%= form_tag({:controller => 'calendars', :action => 'show', :project_id => @project},
4 :method => :get, :id => 'query_form') do %>
4 <%= hidden_field_tag 'set_filter', '1' %>
5 <%= hidden_field_tag 'set_filter', '1' %>
5 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
6 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
6 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
@@ -1,8 +1,8
1 xml.instruct!
1 xml.instruct!
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
2 xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
3 xml.title truncate_single_line(@title, :length => 100)
3 xml.title truncate_single_line(@title, :length => 100)
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false, :escape => false))
4 xml.link "rel" => "self", "href" => url_for(params.merge(:only_path => false))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil, :escape => false))
5 xml.link "rel" => "alternate", "href" => url_for(params.merge(:only_path => false, :format => nil, :key => nil))
6 xml.id url_for(:controller => 'welcome', :only_path => false)
6 xml.id url_for(:controller => 'welcome', :only_path => false)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
7 xml.updated((@items.first ? @items.first.event_datetime : Time.now).xmlschema)
8 xml.author { xml.name "#{Setting.app_title}" }
8 xml.author { xml.name "#{Setting.app_title}" }
@@ -2,7 +2,7
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
3 &#187; <%=h @custom_field.name %></h2>
3 &#187; <%=h @custom_field.name %></h2>
4
4
5 <% labelled_form_for :custom_field, @custom_field, :url => custom_field_path(@custom_field), :html => {:method => :put} do |f| %>
5 <%= labelled_form_for :custom_field, @custom_field, :url => custom_field_path(@custom_field), :html => {:method => :put} do |f| %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
7 <%= submit_tag l(:button_save) %>
7 <%= submit_tag l(:button_save) %>
8 <% end %>
8 <% end %>
@@ -2,7 +2,7
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
2 &#187; <%= link_to l(@custom_field.type_name), :controller => 'custom_fields', :action => 'index', :tab => @custom_field.class.name %>
3 &#187; <%= l(:label_custom_field_new) %></h2>
3 &#187; <%= l(:label_custom_field_new) %></h2>
4
4
5 <% labelled_form_for :custom_field, @custom_field, :url => custom_fields_path do |f| %>
5 <%= labelled_form_for :custom_field, @custom_field, :url => custom_fields_path do |f| %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
6 <%= render :partial => 'form', :locals => { :f => f } %>
7 <%= hidden_field_tag 'type', @custom_field.type %>
7 <%= hidden_field_tag 'type', @custom_field.type %>
8 <%= submit_tag l(:button_save) %>
8 <%= submit_tag l(:button_save) %>
@@ -1,6 +1,6
1 <h2><%=l(:label_document)%></h2>
1 <h2><%=l(:label_document)%></h2>
2
2
3 <% labelled_form_for @document do |f| %>
3 <%= labelled_form_for @document do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <p><%= submit_tag l(:button_save) %></p>
5 <p><%= submit_tag l(:button_save) %></p>
6 <% end %>
6 <% end %>
@@ -5,7 +5,7
5
5
6 <div id="add-document" style="display:none;">
6 <div id="add-document" style="display:none;">
7 <h2><%=l(:label_document_new)%></h2>
7 <h2><%=l(:label_document_new)%></h2>
8 <% labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
8 <%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
9 <%= render :partial => 'form', :locals => {:f => f} %>
9 <%= render :partial => 'form', :locals => {:f => f} %>
10 <p>
10 <p>
11 <%= submit_tag l(:button_create) %>
11 <%= submit_tag l(:button_create) %>
@@ -1,6 +1,6
1 <h2><%=l(:label_document_new)%></h2>
1 <h2><%=l(:label_document_new)%></h2>
2
2
3 <% labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
3 <%= labelled_form_for @document, :url => project_documents_path(@project), :html => {:multipart => true} do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <p><%= submit_tag l(:button_create) %></p>
5 <p><%= submit_tag l(:button_create) %></p>
6 <% end %>
6 <% end %>
@@ -19,7 +19,7
19 <% if authorize_for('documents', 'add_attachment') %>
19 <% if authorize_for('documents', 'add_attachment') %>
20 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
20 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
21 :id => 'attach_files_link' %></p>
21 :id => 'attach_files_link' %></p>
22 <% form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
22 <%= form_tag({ :controller => 'documents', :action => 'add_attachment', :id => @document }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
23 <div class="box">
23 <div class="box">
24 <p><%= render :partial => 'attachments/form' %></p>
24 <p><%= render :partial => 'attachments/form' %></p>
25 </div>
25 </div>
@@ -1,6 +1,6
1 <h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
1 <h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
2
2
3 <% form_tag({}, :method => :delete) do %>
3 <%= form_tag({}, :method => :delete) do %>
4 <div class="box">
4 <div class="box">
5 <p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
5 <p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
6 <p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
6 <p><label for='reassign_to_id'><%= l(:text_enumeration_category_reassign_to) %></label>
@@ -1,6 +1,6
1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=h @enumeration %></h2>
1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=h @enumeration %></h2>
2
2
3 <% labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %>
3 <%= labelled_form_for :enumeration, @enumeration, :url => enumeration_path(@enumeration), :html => {:method => :put} do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=l(:label_enumeration_new)%></h2>
1 <h2><%= link_to l(@enumeration.option_name), enumerations_path %> &#187; <%=l(:label_enumeration_new)%></h2>
2
2
3 <% labelled_form_for :enumeration, @enumeration, :url => enumerations_path do |f| %>
3 <%= labelled_form_for :enumeration, @enumeration, :url => enumerations_path do |f| %>
4 <%= f.hidden_field :type %>
4 <%= f.hidden_field :type %>
5 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= render :partial => 'form', :locals => {:f => f} %>
6 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create) %>
@@ -1,7 +1,7
1 <h2><%=l(:label_attachment_new)%></h2>
1 <h2><%=l(:label_attachment_new)%></h2>
2
2
3 <%= error_messages_for 'attachment' %>
3 <%= error_messages_for 'attachment' %>
4 <% form_tag(project_files_path(@project), :multipart => true, :class => "tabular") do %>
4 <%= form_tag(project_files_path(@project), :multipart => true, :class => "tabular") do %>
5 <div class="box">
5 <div class="box">
6
6
7 <% if @versions.any? %>
7 <% if @versions.any? %>
@@ -1,7 +1,10
1 <% @gantt.view = self %>
1 <% @gantt.view = self %>
2 <h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
2 <h2><%= @query.new_record? ? l(:label_gantt) : h(@query.name) %></h2>
3
3
4 <% form_tag({:controller => 'gantts', :action => 'show', :project_id => @project, :month => params[:month], :year => params[:year], :months => params[:months]}, :method => :get, :id => 'query_form') do %>
4 <%= form_tag({:controller => 'gantts', :action => 'show',
5 :project_id => @project, :month => params[:month],
6 :year => params[:year], :months => params[:months]},
7 :method => :get, :id => 'query_form') do %>
5 <%= hidden_field_tag 'set_filter', '1' %>
8 <%= hidden_field_tag 'set_filter', '1' %>
6 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
9 <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
7 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
10 <legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend>
@@ -1,4 +1,4
1 <% labelled_form_for @group do |f| %>
1 <%= labelled_form_for @group do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
4 <% end %>
@@ -16,8 +16,9
16 <td class="project"><%=h membership.project %></td>
16 <td class="project"><%=h membership.project %></td>
17 <td class="roles">
17 <td class="roles">
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
18 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
19 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
19 <%= form_for(:membership, :remote => true,
20 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
20 :url => { :action => 'edit_membership', :id => @group, :membership_id => membership },
21 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
21 <p><% roles.each do |role| %>
22 <p><% roles.each do |role| %>
22 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role) %> <%=h role %></label><br />
23 <% end %></p>
24 <% end %></p>
@@ -55,7 +56,7
55 <div class="splitcontentright">
56 <div class="splitcontentright">
56 <% if projects.any? %>
57 <% if projects.any? %>
57 <fieldset><legend><%=l(:label_project_new)%></legend>
58 <fieldset><legend><%=l(:label_project_new)%></legend>
58 <% remote_form_for(:membership, :url => { :action => 'edit_membership', :id => @group }) do %>
59 <%= form_for(:membership, :remote => true, :url => { :action => 'edit_membership', :id => @group }) do %>
59 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
60 <%= label_tag "membership_project_id", l(:description_choose_project), :class => "hidden-for-sighted" %>
60 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
61 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@group, projects) %>
61 <p><%= l(:label_role_plural) %>:
62 <p><%= l(:label_role_plural) %>:
@@ -29,7 +29,8
29 <div class="splitcontentright">
29 <div class="splitcontentright">
30 <% users = User.active.not_in_group(@group).all(:limit => 100) %>
30 <% users = User.active.not_in_group(@group).all(:limit => 100) %>
31 <% if users.any? %>
31 <% if users.any? %>
32 <% remote_form_for(@group, :url => group_users_path(@group), :html => {:method => :post}) do |f| %>
32 <%= form_for(@group, :remote => true, :url => group_users_path(@group),
33 :html => {:method => :post}) do |f| %>
33 <fieldset><legend><%=l(:label_user_new)%></legend>
34 <fieldset><legend><%=l(:label_user_new)%></legend>
34
35
35 <p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
36 <p><%= label_tag "user_search", l(:label_user_search) %><%= text_field_tag 'user_search', nil %></p>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
1 <h2><%= link_to l(:label_group_plural), groups_path %> &#187; <%= l(:label_group_new) %></h2>
2
2
3 <% labelled_form_for @group do |f| %>
3 <%= labelled_form_for @group do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <p>
5 <p>
6 <%= f.submit l(:button_create) %>
6 <%= f.submit l(:button_create) %>
@@ -1,6 +1,6
1 <h2><%=l(:label_issue_category)%>: <%=h @category.name %></h2>
1 <h2><%=l(:label_issue_category)%>: <%=h @category.name %></h2>
2
2
3 <% form_tag(issue_category_path(@category), :method => :delete) do %>
3 <%= form_tag(issue_category_path(@category), :method => :delete) do %>
4 <div class="box">
4 <div class="box">
5 <p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p>
5 <p><strong><%= l(:text_issue_category_destroy_question, @issue_count) %></strong></p>
6 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br />
6 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_issue_category_destroy_assignments) %></label><br />
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=h @issue_status %></h2>
1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=h @issue_status %></h2>
2
2
3 <% labelled_form_for @issue_status do |f| %>
3 <%= labelled_form_for @issue_status do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=l(:label_issue_status_new)%></h2>
1 <h2><%= link_to l(:label_issue_status_plural), issue_statuses_path %> &#187; <%=l(:label_issue_status_new)%></h2>
2
2
3 <% labelled_form_for @issue_status do |f| %>
3 <%= labelled_form_for @issue_status do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
2 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
3 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'new', :issue_id => @issue}, :class => 'icon icon-time-add' %>
3 <%= link_to l(:button_log_time), new_issue_time_entry_path(@issue), :class => 'icon icon-time-add' if User.current.allowed_to?(:log_time, @project) %>
4 <%= watcher_tag(@issue, User.current) %>
4 <%= watcher_tag(@issue, User.current) %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %>
5 <%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue}, :class => 'icon icon-copy' %>
6 <%= link_to l(:button_delete), issue_path(@issue), :confirm => issues_destroy_confirmation_message(@issue), :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %>
6 <%= link_to l(:button_delete), issue_path(@issue), :confirm => issues_destroy_confirmation_message(@issue), :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_issues, @project) %>
@@ -1,4 +1,4
1 <% labelled_fields_for :issue, @issue do |f| %>
1 <%= labelled_fields_for :issue, @issue do |f| %>
2
2
3 <div class="splitcontent">
3 <div class="splitcontent">
4 <div class="splitcontentleft">
4 <div class="splitcontentleft">
@@ -1,4 +1,4
1 <% labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
1 <%= labelled_form_for @issue, :html => {:id => 'issue-form', :multipart => true} do |f| %>
2 <%= error_messages_for 'issue', 'time_entry' %>
2 <%= error_messages_for 'issue', 'time_entry' %>
3 <%= render :partial => 'conflict' if @conflict %>
3 <%= render :partial => 'conflict' if @conflict %>
4 <div class="box">
4 <div class="box">
@@ -11,7 +11,7
11 <% end %>
11 <% end %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
12 <% if User.current.allowed_to?(:log_time, @project) %>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
13 <fieldset class="tabular"><legend><%= l(:button_log_time) %></legend>
14 <% labelled_fields_for :time_entry, @time_entry do |time_entry| %>
14 <%= labelled_fields_for :time_entry, @time_entry do |time_entry| %>
15 <div class="splitcontentleft">
15 <div class="splitcontentleft">
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
16 <p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
17 </div>
17 </div>
@@ -1,4 +1,4
1 <% labelled_fields_for :issue, @issue do |f| %>
1 <%= labelled_fields_for :issue, @issue do |f| %>
2 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
2 <%= call_hook(:view_issues_form_details_top, { :issue => @issue, :form => f }) %>
3
3
4 <% if @issue.safe_attribute? 'is_private' %>
4 <% if @issue.safe_attribute? 'is_private' %>
@@ -28,7 +28,7
28 <label><%= l(:field_description) %></label>
28 <label><%= l(:field_description) %></label>
29 <%= link_to_function image_tag('edit.png'),
29 <%= link_to_function image_tag('edit.png'),
30 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %>
30 'Element.hide(this); Effect.toggle("issue_description_and_toolbar", "appear", {duration:0.3})' unless @issue.new_record? %>
31 <% content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %>
31 <%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@issue.new_record? ? nil : 'display:none') do %>
32 <%= f.text_area :description,
32 <%= f.text_area :description,
33 :cols => 60,
33 :cols => 60,
34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
34 :rows => (@issue.description.blank? ? 10 : [[10, @issue.description.length / 50].max, 100].min),
@@ -1,4 +1,4
1 <% form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
2 <%= hidden_field_tag 'back_url', url_for(params), :id => nil %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list issues">
4 <table class="list issues">
@@ -27,8 +27,8
27 <% end %>
27 <% end %>
28 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
28 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %> <%= level > 0 ? "idnt idnt-#{level}" : nil %>">
29 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
29 <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
30 <td class="id"><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
30 <td class="id"><%= link_to issue.id, issue_path(issue) %></td>
31 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.css_classes %><% end %>
31 <%= raw query.columns.map {|column| "<td class=\"#{column.css_classes}\">#{column_content(column, issue)}</td>"}.join %>
32 </tr>
32 </tr>
33 <% end -%>
33 <% end -%>
34 </tbody>
34 </tbody>
@@ -1,5 +1,5
1 <% if issues && issues.any? %>
1 <% if issues && issues.any? %>
2 <% form_tag({}) do %>
2 <%= form_tag({}) do %>
3 <table class="list issues">
3 <table class="list issues">
4 <thead><tr>
4 <thead><tr>
5 <th>#</th>
5 <th>#</th>
@@ -7,7 +7,7
7 ) + h(": #{i.subject}"))
7 ) + h(": #{i.subject}"))
8 }.join("\n").html_safe %></ul>
8 }.join("\n").html_safe %></ul>
9
9
10 <% form_tag({:action => 'bulk_update'}, :id => 'bulk_edit_form') do %>
10 <%= form_tag({:action => 'bulk_update'}, :id => 'bulk_edit_form') do %>
11 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
11 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
12 <div class="box tabular">
12 <div class="box tabular">
13 <fieldset class="attributes">
13 <fieldset class="attributes">
@@ -1,6 +1,6
1 <h2><%= l(:label_confirmation) %></h2>
1 <h2><%= l(:label_confirmation) %></h2>
2
2
3 <% form_tag({}, :method => :delete) do %>
3 <%= form_tag({}, :method => :delete) do %>
4 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
4 <%= @issues.collect {|i| hidden_field_tag('ids[]', i.id)}.join("\n").html_safe %>
5 <div class="box">
5 <div class="box">
6 <p><strong><%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %></strong></p>
6 <p><strong><%= l(:text_destroy_time_entries_question, :hours => number_with_precision(@hours, :precision => 2)) %></strong></p>
@@ -9,7 +9,7
9 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
9 <h2><%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %></h2>
10 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
10 <% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %>
11
11
12 <% form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
12 <%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
13 :method => :get, :id => 'query_form') do %>
13 :method => :get, :id => 'query_form') do %>
14 <%= hidden_field_tag 'set_filter', '1' %>
14 <%= hidden_field_tag 'set_filter', '1' %>
15 <div id="query_form_content" class="hide-when-print">
15 <div id="query_form_content" class="hide-when-print">
@@ -68,7 +68,7
68
68
69 <div id="csv-export-options" style="display:none;">
69 <div id="csv-export-options" style="display:none;">
70 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
70 <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
71 <% form_tag(params.merge({:format => 'csv',:page=>nil}), :method => :get, :id => 'csv-export-form') do %>
71 <%= form_tag(params.merge({:format => 'csv',:page=>nil}), :method => :get, :id => 'csv-export-form') do %>
72 <p>
72 <p>
73 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
73 <label><%= radio_button_tag 'columns', '', true %> <%= l(:description_selected_columns) %></label><br />
74 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
74 <label><%= radio_button_tag 'columns', 'all' %> <%= l(:description_all_columns) %></label>
@@ -2,7 +2,7
2
2
3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
3 <%= call_hook(:view_issues_new_top, {:issue => @issue}) %>
4
4
5 <% labelled_form_for @issue, :url => project_issues_path(@project),
5 <%= labelled_form_for @issue, :url => project_issues_path(@project),
6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
6 :html => {:id => 'issue-form', :multipart => true} do |f| %>
7 <%= error_messages_for 'issue' %>
7 <%= error_messages_for 'issue' %>
8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
8 <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %>
@@ -1,4 +1,4
1 <% form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
1 <%= form_remote_tag(:url => {}, :html => { :id => "journal-#{@journal.id}-form" }) do %>
2 <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %>
2 <%= label_tag "notes", l(:description_notes), :class => "hidden-for-sighted" %>
3 <%= text_area_tag :notes, @journal.notes,
3 <%= text_area_tag :notes, @journal.notes,
4 :id => "journal_#{@journal.id}_notes",
4 :id => "journal_#{@journal.id}_notes",
@@ -29,17 +29,14
29 <div id="account">
29 <div id="account">
30 <%= render_menu :account_menu -%>
30 <%= render_menu :account_menu -%>
31 </div>
31 </div>
32 <%= content_tag(
32 <%= content_tag('div', "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe, :id => 'loggedas') if User.current.logged? %>
33 'div',
34 "#{l(:label_logged_as)} #{link_to_user(User.current, :format => :username)}".html_safe,
35 :id => 'loggedas') if User.current.logged? %>
36 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
33 <%= render_menu :top_menu if User.current.logged? || !Setting.login_required? -%>
37 </div>
34 </div>
38
35
39 <div id="header">
36 <div id="header">
40 <% if User.current.logged? || !Setting.login_required? %>
37 <% if User.current.logged? || !Setting.login_required? %>
41 <div id="quick-search">
38 <div id="quick-search">
42 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
39 <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
43 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
40 <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
44 <label for='q'>
41 <label for='q'>
45 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
42 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
@@ -59,10 +56,13
59 <% end %>
56 <% end %>
60 </div>
57 </div>
61
58
59 <% content_for :sidebar do %>
60 <%= call_hook :view_layouts_base_sidebar %>
61 <% end %>
62
62 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
63 <div id="sidebar">
64 <div id="sidebar">
64 <%= yield :sidebar %>
65 <%= yield :sidebar %>
65 <%= call_hook :view_layouts_base_sidebar %>
66 </div>
66 </div>
67
67
68 <div id="content">
68 <div id="content">
@@ -2,11 +2,13
2 :action => 'show', :project_id => @project,
2 :action => 'show', :project_id => @project,
3 :id => @board %> &#187; <%= h @message.subject %></h2>
3 :id => @board %> &#187; <%= h @message.subject %></h2>
4
4
5 <% form_for :message, @message,
5 <%= form_for @message, {
6 :url => {:action => 'edit'},
6 :as => :message,
7 :html => {:multipart => true,
7 :url => {:action => 'edit'},
8 :id => 'message-form',
8 :html => {:multipart => true,
9 :method => :post} do |f| %>
9 :id => 'message-form',
10 :method => :post}
11 } do |f| %>
10 <%= render :partial => 'form',
12 <%= render :partial => 'form',
11 :locals => {:f => f, :replying => !@message.parent.nil?} %>
13 :locals => {:f => f, :replying => !@message.parent.nil?} %>
12 <%= submit_tag l(:button_save) %>
14 <%= submit_tag l(:button_save) %>
@@ -1,6 +1,6
1 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
1 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
2
2
3 <% form_for :message, @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
3 <%= form_for @message, :url => {:action => 'new'}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= link_to_remote l(:label_preview),
6 <%= link_to_remote l(:label_preview),
@@ -72,7 +72,7
72 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
72 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
73 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
73 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
74 <div id="reply" style="display:none;">
74 <div id="reply" style="display:none;">
75 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
75 <%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
76 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
76 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
77 <%= submit_tag l(:button_submit) %>
77 <%= submit_tag l(:button_submit) %>
78 <%= link_to_remote l(:label_preview),
78 <%= link_to_remote l(:label_preview),
@@ -6,7 +6,7
6 <h2><%=l(:label_my_account)%></h2>
6 <h2><%=l(:label_my_account)%></h2>
7 <%= error_messages_for 'user' %>
7 <%= error_messages_for 'user' %>
8
8
9 <% labelled_form_for :user, @user,
9 <%= labelled_form_for :user, @user,
10 :url => { :action => "account" },
10 :url => { :action => "account" },
11 :html => { :id => 'my_account_form',
11 :html => { :id => 'my_account_form',
12 :method => :post } do |f| %>
12 :method => :post } do |f| %>
@@ -2,7 +2,7
2 <div class="warning">
2 <div class="warning">
3 <p><%= simple_format l(:text_account_destroy_confirmation)%></p>
3 <p><%= simple_format l(:text_account_destroy_confirmation)%></p>
4 <p>
4 <p>
5 <% form_tag({}) do %>
5 <%= form_tag({}) do %>
6 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
6 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
7 <%= submit_tag l(:button_delete_my_account) %> |
7 <%= submit_tag l(:button_delete_my_account) %> |
8 <%= link_to l(:button_cancel), :action => 'account' %>
8 <%= link_to l(:button_cancel), :action => 'account' %>
@@ -35,7 +35,7 function removeBlock(block) {
35 </script>
35 </script>
36
36
37 <div class="contextual">
37 <div class="contextual">
38 <% form_tag({:action => "add_block"}, :id => "block-form") do %>
38 <%= form_tag({:action => "add_block"}, :id => "block-form") do %>
39 <%= label_tag('block-select', l(:label_my_page_block)) %>:
39 <%= label_tag('block-select', l(:label_my_page_block)) %>:
40 <%= select_tag 'block',
40 <%= select_tag 'block',
41 "<option></option>".html_safe + options_for_select(@block_options),
41 "<option></option>".html_safe + options_for_select(@block_options),
@@ -2,7 +2,7
2
2
3 <%= error_messages_for 'user' %>
3 <%= error_messages_for 'user' %>
4
4
5 <% form_tag({}, :class => "tabular") do %>
5 <%= form_tag({}, :class => "tabular") do %>
6 <div class="box">
6 <div class="box">
7 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
7 <p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
8 <%= password_field_tag 'password', nil, :size => 25 %></p>
8 <%= password_field_tag 'password', nil, :size => 25 %></p>
@@ -1,6 +1,6
1 <h2><%=l(:label_news)%></h2>
1 <h2><%=l(:label_news)%></h2>
2
2
3 <% labelled_form_for @news, :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
3 <%= labelled_form_for @news, :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <%= link_to_remote l(:label_preview),
6 <%= link_to_remote l(:label_preview),
@@ -7,7 +7,7
7
7
8 <div id="add-news" style="display:none;">
8 <div id="add-news" style="display:none;">
9 <h2><%=l(:label_news_new)%></h2>
9 <h2><%=l(:label_news_new)%></h2>
10 <% labelled_form_for @news, :url => project_news_index_path(@project),
10 <%= labelled_form_for @news, :url => project_news_index_path(@project),
11 :html => { :id => 'news-form', :multipart => true } do |f| %>
11 :html => { :id => 'news-form', :multipart => true } do |f| %>
12 <%= render :partial => 'news/form', :locals => { :f => f } %>
12 <%= render :partial => 'news/form', :locals => { :f => f } %>
13 <%= submit_tag l(:button_create) %>
13 <%= submit_tag l(:button_create) %>
@@ -1,6 +1,6
1 <h2><%=l(:label_news_new)%></h2>
1 <h2><%=l(:label_news_new)%></h2>
2
2
3 <% labelled_form_for @news, :url => project_news_index_path(@project),
3 <%= labelled_form_for @news, :url => project_news_index_path(@project),
4 :html => { :id => 'news-form', :multipart => true } do |f| %>
4 :html => { :id => 'news-form', :multipart => true } do |f| %>
5 <%= render :partial => 'news/form', :locals => { :f => f } %>
5 <%= render :partial => 'news/form', :locals => { :f => f } %>
6 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create) %>
@@ -16,7 +16,7
16
16
17 <% if authorize_for('news', 'edit') %>
17 <% if authorize_for('news', 'edit') %>
18 <div id="edit-news" style="display:none;">
18 <div id="edit-news" style="display:none;">
19 <% labelled_form_for :news, @news, :url => news_path(@news),
19 <%= labelled_form_for :news, @news, :url => news_path(@news),
20 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
20 :html => { :id => 'news-form', :multipart => true, :method => :put } do |f| %>
21 <%= render :partial => 'form', :locals => { :f => f } %>
21 <%= render :partial => 'form', :locals => { :f => f } %>
22 <%= submit_tag l(:button_save) %>
22 <%= submit_tag l(:button_save) %>
@@ -55,7 +55,7
55
55
56 <% if @news.commentable? %>
56 <% if @news.commentable? %>
57 <p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
57 <p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments" %></p>
58 <% form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
58 <%= form_tag({:controller => 'comments', :action => 'create', :id => @news}, :id => "add_comment_form", :style => "display:none;") do %>
59 <div class="box">
59 <div class="box">
60 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
60 <%= text_area 'comment', 'comments', :cols => 80, :rows => 15, :class => 'wiki-edit' %>
61 <%= wikitoolbar_for 'comment_comments' %>
61 <%= wikitoolbar_for 'comment_comments' %>
@@ -1,4 +1,4
1 <% labelled_form_for @project do |f| %>
1 <%= labelled_form_for @project do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <%= submit_tag l(:button_save) %>
3 <%= submit_tag l(:button_save) %>
4 <% end %>
4 <% end %>
@@ -1,6 +1,6
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_form_for @project, :url => { :action => "copy" } do |f| %>
3 <%= labelled_form_for @project, :url => { :action => "copy" } do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5
5
6 <fieldset class="box tabular"><legend><%= l(:button_copy) %></legend>
6 <fieldset class="box tabular"><legend><%= l(:button_copy) %></legend>
@@ -8,7 +8,7
8 <% end %>
8 <% end %>
9 </p>
9 </p>
10 <p>
10 <p>
11 <% form_tag(project_path(@project_to_destroy), :method => :delete) do %>
11 <%= form_tag(project_path(@project_to_destroy), :method => :delete) do %>
12 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
12 <label><%= check_box_tag 'confirm', 1 %> <%= l(:general_text_Yes) %></label>
13 <%= submit_tag l(:button_delete) %>
13 <%= submit_tag l(:button_delete) %>
14 <% end %>
14 <% end %>
@@ -4,7 +4,7
4
4
5 <div class="contextual">
5 <div class="contextual">
6 <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
6 <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'new'}, :class => 'icon icon-add') + ' |' if User.current.allowed_to?(:add_project, nil, :global => true) %>
7 <%= link_to(l(:label_issue_view_all), { :controller => 'issues' }) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
7 <%= link_to(l(:label_issue_view_all), issues_path) + ' |' if User.current.allowed_to?(:view_issues, nil, :global => true) %>
8 <%= link_to(l(:label_overall_spent_time), time_entries_path) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
8 <%= link_to(l(:label_overall_spent_time), time_entries_path) + ' |' if User.current.allowed_to?(:view_time_entries, nil, :global => true) %>
9 <%= link_to l(:label_overall_activity),
9 <%= link_to l(:label_overall_activity),
10 { :controller => 'activities', :action => 'index',
10 { :controller => 'activities', :action => 'index',
@@ -1,6 +1,6
1 <h2><%=l(:label_project_new)%></h2>
1 <h2><%=l(:label_project_new)%></h2>
2
2
3 <% labelled_form_for @project do |f| %>
3 <%= labelled_form_for @project do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
@@ -1,4 +1,4
1 <% form_tag(project_enumerations_path(@project), :method => :put, :class => "tabular") do %>
1 <%= form_tag(project_enumerations_path(@project), :method => :put, :class => "tabular") do %>
2
2
3 <table class="list">
3 <table class="list">
4 <thead><tr>
4 <thead><tr>
@@ -11,7 +11,7
11 </tr></thead>
11 </tr></thead>
12
12
13 <% @project.activities(true).each do |enumeration| %>
13 <% @project.activities(true).each do |enumeration| %>
14 <% fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %>
14 <%= fields_for "enumerations[#{enumeration.id}]", enumeration do |ff| %>
15 <tr class="<%= cycle('odd', 'even') %>">
15 <tr class="<%= cycle('odd', 'even') %>">
16 <td>
16 <td>
17 <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
17 <%= ff.hidden_field :parent_id, :value => enumeration.id unless enumeration.project %>
@@ -1,4 +1,4
1 <% form_for :project, @project,
1 <%= form_for @project,
2 :url => { :action => 'modules', :id => @project },
2 :url => { :action => 'modules', :id => @project },
3 :html => {:id => 'modules-form',
3 :html => {:id => 'modules-form',
4 :method => :post} do |f| %>
4 :method => :post} do |f| %>
@@ -12,7 +12,7
12 </div>
12 </div>
13 <ul>
13 <ul>
14 <% unless @project.homepage.blank? %>
14 <% unless @project.homepage.blank? %>
15 <li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)).html_safe %></li>
15 <li><%=l(:field_homepage)%>: <%= link_to h(@project.homepage), @project.homepage %></li>
16 <% end %>
16 <% end %>
17 <% if @subprojects.any? %>
17 <% if @subprojects.any? %>
18 <li><%=l(:label_subproject_plural)%>:
18 <li><%=l(:label_subproject_plural)%>:
@@ -69,8 +69,8
69 <% if @total_hours.present? %>
69 <% if @total_hours.present? %>
70 <h3><%= l(:label_spent_time) %></h3>
70 <h3><%= l(:label_spent_time) %></h3>
71 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
71 <p><span class="icon icon-time"><%= l_hours(@total_hours) %></span></p>
72 <p><%= link_to(l(:label_details), {:controller => 'timelog', :action => 'index', :project_id => @project}) %> |
72 <p><%= link_to(l(:label_details), project_time_entries_path(@project)) %> |
73 <%= link_to(l(:label_report), {:controller => 'timelog', :action => 'report', :project_id => @project}) %></p>
73 <%= link_to(l(:label_report), report_project_time_entries_path(@project)) %></p>
74 <% end %>
74 <% end %>
75 <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
75 <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %>
76 <% end %>
76 <% end %>
@@ -5,7 +5,7
5 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
5 <% query.available_filters.sort{|a,b| a[1][:order]<=>b[1][:order]}.each do |filter| %>
6 <% field = filter[0]
6 <% field = filter[0]
7 options = filter[1] %>
7 options = filter[1] %>
8 <tr <%= 'style="display:none;"' unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
8 <tr <%= 'style="display:none;"'.html_safe unless query.has_filter?(field) %> id="tr_<%= field %>" class="filter">
9 <td class="field">
9 <td class="field">
10 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
10 <%= check_box_tag 'f[]', field, query.has_filter?(field), :onclick => "toggle_filter('#{field}');", :id => "cb_#{field}" %>
11 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
11 <label for="cb_<%= field %>"><%= filter[1][:name] || l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) %></label>
@@ -1,6 +1,6
1 <h2><%= l(:label_query) %></h2>
1 <h2><%= l(:label_query) %></h2>
2
2
3 <% form_tag(query_path(@query), :onsubmit => 'selectAllOptions("selected_columns");', :method => :put) do %>
3 <%= form_tag(query_path(@query), :onsubmit => 'selectAllOptions("selected_columns");', :method => :put) do %>
4 <%= render :partial => 'form', :locals => {:query => @query} %>
4 <%= render :partial => 'form', :locals => {:query => @query} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= l(:label_query_new) %></h2>
1 <h2><%= l(:label_query_new) %></h2>
2
2
3 <% form_tag(@project ? project_queries_path : queries_path, :onsubmit => 'selectAllOptions("selected_columns");') do %>
3 <%= form_tag(@project ? project_queries_path(@project) : queries_path, :onsubmit => 'selectAllOptions("selected_columns");') do %>
4 <%= render :partial => 'form', :locals => {:query => @query} %>
4 <%= render :partial => 'form', :locals => {:query => @query} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -6,7 +6,7
6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
6 {:action => 'stats', :id => @project, :repository_id => @repository.identifier_param},
7 :class => 'icon icon-stats' if @repository.supports_all_revisions? %>
7 :class => 'icon icon-stats' if @repository.supports_all_revisions? %>
8
8
9 <% form_tag({:action => controller.action_name,
9 <%= form_tag({:action => controller.action_name,
10 :id => @project,
10 :id => @project,
11 :repository_id => @repository.identifier_param,
11 :repository_id => @repository.identifier_param,
12 :path => to_path_param(@path),
12 :path => to_path_param(@path),
@@ -14,7 +14,7
14 :space => graph_space
14 :space => graph_space
15 }
15 }
16 end %>
16 end %>
17 <% form_tag(
17 <%= form_tag(
18 {:controller => 'repositories', :action => 'diff', :id => project,
18 {:controller => 'repositories', :action => 'diff', :id => project,
19 :repository_id => @repository.identifier_param, :path => to_path_param(path)},
19 :repository_id => @repository.identifier_param, :path => to_path_param(path)},
20 :method => :get
20 :method => :get
@@ -6,7 +6,7
6 <p class="nodata"><%= l(:label_no_data) %></p>
6 <p class="nodata"><%= l(:label_no_data) %></p>
7 <% else %>
7 <% else %>
8
8
9 <% form_tag({}) do %>
9 <%= form_tag({}) do %>
10 <table class="list">
10 <table class="list">
11 <thead>
11 <thead>
12 <tr>
12 <tr>
@@ -1,7 +1,7
1 <h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=h @path %></h2>
1 <h2><%= l(:label_revision) %> <%= @diff_format_revisions %> <%=h @path %></h2>
2
2
3 <!-- Choose view type -->
3 <!-- Choose view type -->
4 <% form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
4 <%= form_tag({:path => to_path_param(@path)}, :method => 'get') do %>
5 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
5 <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
6 <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
7 <p>
7 <p>
@@ -1,5 +1,5
1 <h2><%= l(:label_repository) %></h2>
1 <h2><%= l(:label_repository) %></h2>
2
2
3 <% labelled_form_for :repository, @repository, :url => repository_path(@path), :html => {:method => :put} do |f| %>
3 <%= labelled_form_for :repository, @repository, :url => repository_path(@path), :html => {:method => :put} do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <% end %>
5 <% end %>
@@ -1,5 +1,5
1 <h2><%= l(:label_repository_new) %></h2>
1 <h2><%= l(:label_repository_new) %></h2>
2
2
3 <% labelled_form_for :repository, @repository, :url => project_repositories_path(@project) do |f| %>
3 <%= labelled_form_for :repository, @repository, :url => project_repositories_path(@project) do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <% end %>
5 <% end %>
@@ -13,7 +13,7
13 <% end -%>
13 <% end -%>
14 &#187;&nbsp;
14 &#187;&nbsp;
15
15
16 <% form_tag({:controller => 'repositories',
16 <%= form_tag({:controller => 'repositories',
17 :action => 'revision',
17 :action => 'revision',
18 :id => @project,
18 :id => @project,
19 :repository_id => @repository.identifier_param,
19 :repository_id => @repository.identifier_param,
@@ -1,6 +1,6
1 <div class="contextual">
1 <div class="contextual">
2 <% form_tag(
2 <%= form_tag(
3 {:action => 'revision', :id => @project,
3 {:controller => 'repositories', :action => 'revision', :id => @project,
4 :repository_id => @repository.identifier_param}
4 :repository_id => @repository.identifier_param}
5 ) do %>
5 ) do %>
6 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
6 <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 8 %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=h @role.name %></h2>
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=h @role.name %></h2>
2
2
3 <% labelled_form_for @role do |f| %>
3 <%= labelled_form_for @role do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_role_new)%></h2>
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_role_new)%></h2>
2
2
3 <% labelled_form_for @role do |f| %>
3 <%= labelled_form_for @role do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_permissions_report)%></h2>
1 <h2><%= link_to l(:label_role_plural), roles_path %> &#187; <%=l(:label_permissions_report)%></h2>
2
2
3 <% form_tag(permissions_roles_path, :id => 'permissions_form') do %>
3 <%= form_tag(permissions_roles_path, :id => 'permissions_form') do %>
4 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
4 <%= hidden_field_tag 'permissions[0]', '', :id => nil %>
5 <div class="autoscroll">
5 <div class="autoscroll">
6 <table class="list permissions">
6 <table class="list permissions">
@@ -1,7 +1,7
1 <h2><%= l(:label_search) %></h2>
1 <h2><%= l(:label_search) %></h2>
2
2
3 <div class="box">
3 <div class="box">
4 <% form_tag({}, :method => :get) do %>
4 <%= form_tag({}, :method => :get) do %>
5 <%= label_tag "search-input", l(:description_search), :class => "hidden-for-sighted" %>
5 <%= label_tag "search-input", l(:description_search), :class => "hidden-for-sighted" %>
6 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
6 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
7 <%= javascript_tag "Field.focus('search-input')" %>
7 <%= javascript_tag "Field.focus('search-input')" %>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit', :tab => 'authentication'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'authentication'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :login_required %></p>
4 <p><%= setting_check_box :login_required %></p>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit', :tab => 'display'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'display'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_select :ui_theme, Redmine::Themes.themes.collect {|t| [t.name, t.id]}, :blank => :label_default, :label => :label_theme %></p>
4 <p><%= setting_select :ui_theme, Redmine::Themes.themes.collect {|t| [t.name, t.id]}, :blank => :label_default, :label => :label_theme %></p>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit'}) do %>
1 <%= form_tag({:action => 'edit'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_text_field :app_title, :size => 30 %></p>
4 <p><%= setting_text_field :app_title, :size => 30 %></p>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit', :tab => 'issues'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'issues'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :cross_project_issue_relations %></p>
4 <p><%= setting_check_box :cross_project_issue_relations %></p>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'mail_handler'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p>
4 <p>
@@ -1,5 +1,5
1 <% if @deliveries %>
1 <% if @deliveries %>
2 <% form_tag({:action => 'edit', :tab => 'notifications'}) do %>
2 <%= form_tag({:action => 'edit', :tab => 'notifications'}) do %>
3
3
4 <div class="box tabular settings">
4 <div class="box tabular settings">
5 <p><%= setting_text_field :mail_from, :size => 60 %></p>
5 <p><%= setting_text_field :mail_from, :size => 60 %></p>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit', :tab => 'projects'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'projects'}) do %>
2
2
3 <div class="box tabular settings">
3 <div class="box tabular settings">
4 <p><%= setting_check_box :default_projects_public %></p>
4 <p><%= setting_check_box :default_projects_public %></p>
@@ -1,4 +1,4
1 <% form_tag({:action => 'edit', :tab => 'repositories'}) do %>
1 <%= form_tag({:action => 'edit', :tab => 'repositories'}) do %>
2
2
3 <fieldset class="box settings enabled_scm">
3 <fieldset class="box settings enabled_scm">
4 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
4 <%= hidden_field_tag 'settings[enabled_scm][]', '' %>
@@ -1,7 +1,7
1 <h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2>
1 <h2><%= l(:label_settings) %>: <%=h @plugin.name %></h2>
2
2
3 <div id="settings">
3 <div id="settings">
4 <% form_tag({:action => 'plugin'}) do %>
4 <%= form_tag({:action => 'plugin'}) do %>
5 <div class="box tabular">
5 <div class="box tabular">
6 <%= render :partial => @partial, :locals => {:settings => @settings}%>
6 <%= render :partial => @partial, :locals => {:settings => @settings}%>
7 </div>
7 </div>
@@ -31,8 +31,8
31 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
31 <% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
32 <ul>
32 <ul>
33 <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
33 <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
34 :class => (@controller.action_name == 'index' ? 'selected' : nil)) %></li>
34 :class => (action_name == 'index' ? 'selected' : nil)) %></li>
35 <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
35 <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
36 :class => (@controller.action_name == 'report' ? 'selected' : nil)) %></li>
36 :class => (action_name == 'report' ? 'selected' : nil)) %></li>
37 </ul>
37 </ul>
38 </div>
38 </div>
@@ -1,4 +1,4
1 <% form_tag({}) do -%>
1 <%= form_tag({}) do -%>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 <div class="autoscroll">
3 <div class="autoscroll">
4 <table class="list time-entries">
4 <table class="list time-entries">
@@ -7,7 +7,7
7 )}.join("\n").html_safe %>
7 )}.join("\n").html_safe %>
8 </ul>
8 </ul>
9
9
10 <% form_tag(:action => 'bulk_update') do %>
10 <%= form_tag(:action => 'bulk_update') do %>
11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
11 <%= @time_entries.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %>
12 <div class="box tabular">
12 <div class="box tabular">
13 <div>
13 <div>
@@ -1,6 +1,6
1 <h2><%= l(:label_spent_time) %></h2>
1 <h2><%= l(:label_spent_time) %></h2>
2
2
3 <% labelled_form_for @time_entry, :url => project_time_entry_path(@time_entry.project, @time_entry) do |f| %>
3 <%= labelled_form_for @time_entry, :url => project_time_entry_path(@time_entry.project, @time_entry) do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -8,7 +8,7
8
8
9 <h2><%= l(:label_spent_time) %></h2>
9 <h2><%= l(:label_spent_time) %></h2>
10
10
11 <% form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
11 <%= form_tag({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
12 <%= render :partial => 'date_range' %>
12 <%= render :partial => 'date_range' %>
13 <% end %>
13 <% end %>
14
14
@@ -1,6 +1,6
1 <h2><%= l(:label_spent_time) %></h2>
1 <h2><%= l(:label_spent_time) %></h2>
2
2
3 <% labelled_form_for @time_entry, :url => time_entries_path do |f| %>
3 <%= labelled_form_for @time_entry, :url => time_entries_path do |f| %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
4 <%= render :partial => 'form', :locals => {:f => f} %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
6 <%= submit_tag l(:button_create_and_continue), :name => 'continue' %>
@@ -6,7 +6,9
6
6
7 <h2><%= l(:label_spent_time) %></h2>
7 <h2><%= l(:label_spent_time) %></h2>
8
8
9 <% form_tag({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}, :method => :get, :id => 'query_form') do %>
9 <%= form_tag({:controller => 'timelog', :action => 'report',
10 :project_id => @project, :issue_id => @issue},
11 :method => :get, :id => 'query_form') do %>
10 <% @report.criteria.each do |criterion| %>
12 <% @report.criteria.each do |criterion| %>
11 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
13 <%= hidden_field_tag 'criteria[]', criterion, :id => nil %>
12 <% end %>
14 <% end %>
@@ -1,5 +1,5
1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=h @tracker %></h2>
1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=h @tracker %></h2>
2
2
3 <% labelled_form_for @tracker do |f| %>
3 <%= labelled_form_for @tracker do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% end %>
5 <% end %>
@@ -1,5 +1,5
1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=l(:label_tracker_new)%></h2>
1 <h2><%= link_to l(:label_tracker_plural), trackers_path %> &#187; <%=l(:label_tracker_new)%></h2>
2
2
3 <% labelled_form_for @tracker do |f| %>
3 <%= labelled_form_for @tracker do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% end %>
5 <% end %>
@@ -1,4 +1,4
1 <% labelled_form_for @user do |f| %>
1 <%= labelled_form_for @user do |f| %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
2 <%= render :partial => 'form', :locals => { :f => f } %>
3 <% if @user.active? && email_delivery_enabled? -%>
3 <% if @user.active? && email_delivery_enabled? -%>
4 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
4 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
@@ -1,4 +1,4
1 <% form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>
1 <%= form_for(:user, :url => { :action => 'update' }, :html => {:method => :put}) do %>
2 <div class="box">
2 <div class="box">
3 <% Group.all.sort.each do |group| %>
3 <% Group.all.sort.each do |group| %>
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
4 <label><%= check_box_tag 'user[group_ids][]', group.id, @user.groups.include?(group) %> <%=h group %></label><br />
@@ -19,8 +19,10
19 </td>
19 </td>
20 <td class="roles">
20 <td class="roles">
21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
21 <span id="member-<%= membership.id %>-roles"><%=h membership.roles.sort.collect(&:to_s).join(', ') %></span>
22 <% remote_form_for(:membership, :url => user_membership_path(@user, membership), :method => :put,
22 <%= form_for(:membership, :remote => true,
23 :html => { :id => "member-#{membership.id}-roles-form", :style => 'display:none;'}) do %>
23 :url => user_membership_path(@user, membership), :method => :put,
24 :html => {:id => "member-#{membership.id}-roles-form",
25 :style => 'display:none;'}) do %>
24 <p><% roles.each do |role| %>
26 <p><% roles.each do |role| %>
25 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
27 <label><%= check_box_tag 'membership[role_ids][]', role.id, membership.roles.include?(role),
26 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
28 :disabled => membership.member_roles.detect {|mr| mr.role_id == role.id && !mr.inherited_from.nil?} %> <%=h role %></label><br />
@@ -57,7 +59,7
57 <div class="splitcontentright">
59 <div class="splitcontentright">
58 <% if projects.any? %>
60 <% if projects.any? %>
59 <fieldset><legend><%=l(:label_project_new)%></legend>
61 <fieldset><legend><%=l(:label_project_new)%></legend>
60 <% remote_form_for(:membership, :url => user_memberships_path(@user)) do %>
62 <%= form_for(:membership, :remote => true, :url => user_memberships_path(@user)) do %>
61 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
63 <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, projects) %>
62 <p><%= l(:label_role_plural) %>:
64 <p><%= l(:label_role_plural) %>:
63 <% roles.each do |role| %>
65 <% roles.each do |role| %>
@@ -1,4 +1,4
1 <% labelled_fields_for :pref, @user.pref do |pref_fields| %>
1 <%= labelled_fields_for :pref, @user.pref do |pref_fields| %>
2 <p><%= pref_fields.check_box :hide_mail %></p>
2 <p><%= pref_fields.check_box :hide_mail %></p>
3 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
3 <p><%= pref_fields.select :time_zone, ActiveSupport::TimeZone.all.collect {|z| [ z.to_s, z.name ]}, :include_blank => true %></p>
4 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
4 <p><%= pref_fields.select :comments_sorting, [[l(:label_chronological_order), 'asc'], [l(:label_reverse_chronological_order), 'desc']] %></p>
@@ -4,7 +4,7
4
4
5 <h2><%=l(:label_user_plural)%></h2>
5 <h2><%=l(:label_user_plural)%></h2>
6
6
7 <% form_tag({}, :method => :get) do %>
7 <%= form_tag({}, :method => :get) do %>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
8 <fieldset><legend><%= l(:label_filter_plural) %></legend>
9 <label for='status'><%= l(:field_status) %>:</label>
9 <label for='status'><%= l(:field_status) %>:</label>
10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
10 <%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
@@ -1,6 +1,6
1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
1 <h2><%= link_to l(:label_user_plural), users_path %> &#187; <%=l(:label_user_new)%></h2>
2
2
3 <% labelled_form_for @user do |f| %>
3 <%= labelled_form_for @user do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <% if email_delivery_enabled? %>
5 <% if email_delivery_enabled? %>
6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
6 <p><label><%= check_box_tag 'send_information', 1, true %> <%= l(:label_send_information) %></label></p>
@@ -1,4 +1,4
1 <% form_tag({}, :id => "status_by_form") do -%>
1 <%= form_tag({}, :id => "status_by_form") do -%>
2 <fieldset>
2 <fieldset>
3 <legend>
3 <legend>
4 <%= l(:label_issues_by,
4 <%= l(:label_issues_by,
@@ -1,6 +1,6
1 <h3 class="title"><%=l(:label_version_new)%></h3>
1 <h3 class="title"><%=l(:label_version_new)%></h3>
2
2
3 <% labelled_remote_form_for @version, :url => project_versions_path(@project) do |f| %>
3 <%= labelled_remote_form_for :version, @version, :url => project_versions_path(@project) do |f| %>
4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
5 <p class="buttons">
5 <p class="buttons">
6 <%= submit_tag l(:button_create), :name => nil %>
6 <%= submit_tag l(:button_create), :name => nil %>
@@ -1,6 +1,6
1 <h2><%=l(:label_version)%></h2>
1 <h2><%=l(:label_version)%></h2>
2
2
3 <% labelled_form_for @version do |f| %>
3 <%= labelled_form_for @version do |f| %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
4 <%= render :partial => 'form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_save) %>
5 <%= submit_tag l(:button_save) %>
6 <% end %>
6 <% end %>
@@ -14,7 +14,7
14 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
14 <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
15
15
16 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
16 <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
17 <% form_tag({}) do -%>
17 <%= form_tag({}) do -%>
18 <table class="list related-issues">
18 <table class="list related-issues">
19 <caption><%= l(:label_related_issues) %></caption>
19 <caption><%= l(:label_related_issues) %></caption>
20 <% issues.each do |issue| -%>
20 <% issues.each do |issue| -%>
@@ -32,7 +32,7
32 <% end %>
32 <% end %>
33
33
34 <% content_for :sidebar do %>
34 <% content_for :sidebar do %>
35 <% form_tag({}, :method => :get) do %>
35 <%= form_tag({}, :method => :get) do %>
36 <h3><%= l(:label_roadmap) %></h3>
36 <h3><%= l(:label_roadmap) %></h3>
37 <% @trackers.each do |tracker| %>
37 <% @trackers.each do |tracker| %>
38 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
38 <label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
@@ -1,6 +1,6
1 <h2><%=l(:label_version_new)%></h2>
1 <h2><%=l(:label_version_new)%></h2>
2
2
3 <% labelled_form_for @version, :url => project_versions_path(@project) do |f| %>
3 <%= labelled_form_for @version, :url => project_versions_path(@project) do |f| %>
4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
4 <%= render :partial => 'versions/form', :locals => { :f => f } %>
5 <%= submit_tag l(:button_create) %>
5 <%= submit_tag l(:button_create) %>
6 <% end %>
6 <% end %>
@@ -36,7 +36,7
36 </div>
36 </div>
37
37
38 <% if @issues.present? %>
38 <% if @issues.present? %>
39 <% form_tag({}) do -%>
39 <%= form_tag({}) do -%>
40 <table class="list related-issues">
40 <table class="list related-issues">
41 <caption><%= l(:label_related_issues) %></caption>
41 <caption><%= l(:label_related_issues) %></caption>
42 <%- @issues.each do |issue| -%>
42 <%- @issues.each do |issue| -%>
@@ -1,6 +1,6
1 <h3 class="title"><%= l(:permission_add_issue_watchers) %></h3>
1 <h3 class="title"><%= l(:permission_add_issue_watchers) %></h3>
2
2
3 <% form_remote_tag :url => {:controller => 'watchers',
3 <%= form_remote_tag :url => {:controller => 'watchers',
4 :action => (watched ? 'create' : 'append'),
4 :action => (watched ? 'create' : 'append'),
5 :object_type => watched.class.name.underscore,
5 :object_type => watched.class.name.underscore,
6 :object_id => watched},
6 :object_id => watched},
@@ -6,4 +6,4
6
6
7 <%= link_to l(:field_start_page), {:action => 'show', :id => nil} %><br />
7 <%= link_to l(:field_start_page), {:action => 'show', :id => nil} %><br />
8 <%= link_to l(:label_index_by_title), {:action => 'index'} %><br />
8 <%= link_to l(:label_index_by_title), {:action => 'index'} %><br />
9 <%= link_to l(:label_index_by_date), {:action => 'date_index'} %><br />
9 <%= link_to l(:label_index_by_date), {:controller => 'wiki', :project_id => @project, :action => 'date_index'} %><br />
@@ -2,7 +2,7
2
2
3 <h2><%=h @page.pretty_title %></h2>
3 <h2><%=h @page.pretty_title %></h2>
4
4
5 <% form_tag({}, :method => :delete) do %>
5 <%= form_tag({}, :method => :delete) do %>
6 <div class="box">
6 <div class="box">
7 <p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
7 <p><strong><%= l(:text_wiki_page_destroy_question, :descendants => @descendants_count) %></strong></p>
8 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
8 <p><label><%= radio_button_tag 'todo', 'nullify', true %> <%= l(:text_wiki_page_nullify_children) %></label><br />
@@ -2,7 +2,7
2
2
3 <h2><%= h @page.pretty_title %></h2>
3 <h2><%= h @page.pretty_title %></h2>
4
4
5 <% form_for :content, @content,
5 <%= form_for @content, :as => :content,
6 :url => {:action => 'update', :id => @page.title},
6 :url => {:action => 'update', :id => @page.title},
7 :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
7 :html => {:method => :put, :multipart => true, :id => 'wiki_form'} do |f| %>
8 <%= f.hidden_field :version %>
8 <%= f.hidden_field :version %>
@@ -16,10 +16,10
16 <%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>
16 <%= text_area_tag 'content[text]', @text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit) %>
17
17
18 <% if @page.safe_attribute_names.include?('parent_id') && @wiki.pages.any? %>
18 <% if @page.safe_attribute_names.include?('parent_id') && @wiki.pages.any? %>
19 <% fields_for @page do |fp| %>
19 <%= fields_for @page do |fp| %>
20 <p>
20 <p>
21 <label><%= l(:field_parent_title) %></label>
21 <label><%= l(:field_parent_title) %></label>
22 <%= fp.select :parent_id, "<option value=''></option>" + wiki_page_options_for_select(@wiki.pages.all(:include => :parent) - @page.self_and_descendants, @page.parent) %>
22 <%= fp.select :parent_id, content_tag('option', '', :value => '') + wiki_page_options_for_select(@wiki.pages.all(:include => :parent) - @page.self_and_descendants, @page.parent) %>
23 </p>
23 </p>
24 <% end %>
24 <% end %>
25 <% end %>
25 <% end %>
@@ -4,7 +4,9
4
4
5 <h3><%= l(:label_history) %></h3>
5 <h3><%= l(:label_history) %></h3>
6
6
7 <% form_tag({:controller => 'wiki', :action => 'diff', :project_id => @page.project, :id => @page.title}, :method => :get) do %>
7 <%= form_tag({:controller => 'wiki', :action => 'diff',
8 :project_id => @page.project, :id => @page.title},
9 :method => :get) do %>
8 <table class="list wiki-page-versions">
10 <table class="list wiki-page-versions">
9 <thead><tr>
11 <thead><tr>
10 <th>#</th>
12 <th>#</th>
@@ -4,14 +4,14
4
4
5 <%= error_messages_for 'page' %>
5 <%= error_messages_for 'page' %>
6
6
7 <% labelled_form_for :wiki_page, @page,
7 <%= labelled_form_for :wiki_page, @page,
8 :url => { :action => 'rename' },
8 :url => { :action => 'rename' },
9 :html => { :method => :post } do |f| %>
9 :html => { :method => :post } do |f| %>
10 <div class="box tabular">
10 <div class="box tabular">
11 <p><%= f.text_field :title, :required => true, :size => 100 %></p>
11 <p><%= f.text_field :title, :required => true, :size => 100 %></p>
12 <p><%= f.check_box :redirect_existing_links %></p>
12 <p><%= f.check_box :redirect_existing_links %></p>
13 <p><%= f.select :parent_id,
13 <p><%= f.select :parent_id,
14 "<option value=''></option>" +
14 content_tag('option', '', :value => '') +
15 wiki_page_options_for_select(
15 wiki_page_options_for_select(
16 @wiki.pages.all(:include => :parent) - @page.self_and_descendants,
16 @wiki.pages.all(:include => :parent) - @page.self_and_descendants,
17 @page.parent),
17 @page.parent),
@@ -42,7 +42,10
42 <div id="wiki_add_attachment">
42 <div id="wiki_add_attachment">
43 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
43 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
44 :id => 'attach_files_link' %></p>
44 :id => 'attach_files_link' %></p>
45 <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :project_id => @project, :id => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
45 <%= form_tag({:controller => 'wiki', :action => 'add_attachment',
46 :project_id => @project, :id => @page.title},
47 :multipart => true, :id => "add_attachment_form",
48 :style => "display:none;") do %>
46 <div class="box">
49 <div class="box">
47 <p><%= render :partial => 'attachments/form' %></p>
50 <p><%= render :partial => 'attachments/form' %></p>
48 </div>
51 </div>
@@ -3,7 +3,7
3 <div class="box"><center>
3 <div class="box"><center>
4 <p><strong><%= h(@project.name) %></strong><br /><%=l(:text_wiki_destroy_confirmation)%></p>
4 <p><strong><%= h(@project.name) %></strong><br /><%=l(:text_wiki_destroy_confirmation)%></p>
5
5
6 <% form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %>
6 <%= form_tag({:controller => 'wikis', :action => 'destroy', :id => @project}) do %>
7 <%= hidden_field_tag "confirm", 1 %>
7 <%= hidden_field_tag "confirm", 1 %>
8 <%= submit_tag l(:button_delete) %>
8 <%= submit_tag l(:button_delete) %>
9 <% end %>
9 <% end %>
@@ -2,7 +2,7
2
2
3 <h2><%=l(:label_workflow)%></h2>
3 <h2><%=l(:label_workflow)%></h2>
4
4
5 <% form_tag({}, :id => 'workflow_copy_form') do %>
5 <%= form_tag({}, :id => 'workflow_copy_form') do %>
6 <fieldset class="tabular box">
6 <fieldset class="tabular box">
7 <legend><%= l(:label_copy_source) %></legend>
7 <legend><%= l(:label_copy_source) %></legend>
8 <p>
8 <p>
@@ -4,7 +4,7
4
4
5 <p><%=l(:text_workflow_edit)%>:</p>
5 <p><%=l(:text_workflow_edit)%>:</p>
6
6
7 <% form_tag({}, :method => 'get') do %>
7 <%= form_tag({}, :method => 'get') do %>
8 <p>
8 <p>
9 <label><%=l(:label_role)%>:
9 <label><%=l(:label_role)%>:
10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
10 <%= select_tag 'role_id', options_from_collection_for_select(@roles, "id", "name", @role && @role.id) %></label>
@@ -21,7 +21,7
21 <% end %>
21 <% end %>
22
22
23 <% if @tracker && @role && @statuses.any? %>
23 <% if @tracker && @role && @statuses.any? %>
24 <% form_tag({}, :id => 'workflow_form' ) do %>
24 <%= form_tag({}, :id => 'workflow_form' ) do %>
25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
25 <%= hidden_field_tag 'tracker_id', @tracker.id %>
26 <%= hidden_field_tag 'role_id', @role.id %>
26 <%= hidden_field_tag 'role_id', @role.id %>
27 <div class="autoscroll">
27 <div class="autoscroll">
@@ -1,124 +1,6
1 # Don't change this file!
1 require 'rubygems'
2 # Configure your app in config/environment.rb and config/environments/*.rb
3
2
4 if RUBY_VERSION >= '1.9'
3 # Set up gems listed in the Gemfile.
5 require 'yaml'
4 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
6 YAML::ENGINE.yamler = 'syck'
7 end
8
5
9 RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
6 require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
10
11 module Rails
12 class << self
13 def boot!
14 unless booted?
15 preinitialize
16 pick_boot.run
17 end
18 end
19
20 def booted?
21 defined? Rails::Initializer
22 end
23
24 def pick_boot
25 (vendor_rails? ? VendorBoot : GemBoot).new
26 end
27
28 def vendor_rails?
29 File.exist?("#{RAILS_ROOT}/vendor/rails")
30 end
31
32 def preinitialize
33 load(preinitializer_path) if File.exist?(preinitializer_path)
34 end
35
36 def preinitializer_path
37 "#{RAILS_ROOT}/config/preinitializer.rb"
38 end
39 end
40
41 class Boot
42 def run
43 load_initializer
44 Rails::Initializer.class_eval do
45 def load_gems
46 @bundler_loaded ||= Bundler.require :default, Rails.env
47 end
48 end
49 Rails::Initializer.run(:set_load_path)
50 end
51 end
52
53 class VendorBoot < Boot
54 def load_initializer
55 require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
56 Rails::Initializer.run(:install_gem_spec_stubs)
57 Rails::GemDependency.add_frozen_gem_path
58 end
59 end
60
61 class GemBoot < Boot
62 def load_initializer
63 self.class.load_rubygems
64 load_rails_gem
65 require 'initializer'
66 end
67
68 def load_rails_gem
69 if version = self.class.gem_version
70 gem 'rails', version
71 else
72 gem 'rails'
73 end
74 rescue Gem::LoadError => load_error
75 if load_error.message =~ /Could not find RubyGem rails/
76 STDERR.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
77 exit 1
78 else
79 raise
80 end
81 end
82
83 class << self
84 def rubygems_version
85 Gem::RubyGemsVersion rescue nil
86 end
87
88 def gem_version
89 if defined? RAILS_GEM_VERSION
90 RAILS_GEM_VERSION
91 elsif ENV.include?('RAILS_GEM_VERSION')
92 ENV['RAILS_GEM_VERSION']
93 else
94 parse_gem_version(read_environment_rb)
95 end
96 end
97
98 def load_rubygems
99 min_version = '1.3.2'
100 require 'rubygems'
101 unless rubygems_version >= min_version
102 $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
103 exit 1
104 end
105
106 rescue LoadError
107 $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
108 exit 1
109 end
110
111 def parse_gem_version(text)
112 $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
113 end
114
115 private
116 def read_environment_rb
117 File.read("#{RAILS_ROOT}/config/environment.rb")
118 end
119 end
120 end
121 end
122
123 # All that for this:
124 Rails.boot!
@@ -1,59 +1,5
1 # Be sure to restart your web server when you modify this file.
1 # Load the rails application
2 require File.expand_path('../application', __FILE__)
2
3
3 # Uncomment below to force Rails into production mode when
4 # Initialize the rails application
4 # you don't control web/app server and can't set it the proper way
5 RedmineApp::Application.initialize!
5 # ENV['RAILS_ENV'] ||= 'production'
6
7 # Bootstrap the Rails environment, frameworks, and default configuration
8 require File.join(File.dirname(__FILE__), 'boot')
9
10 if RUBY_VERSION >= '1.9' && defined?(Rails) && Rails::VERSION::MAJOR < 3
11 Encoding.default_external = 'UTF-8'
12 end
13
14 # Load Engine plugin if available
15 begin
16 require File.join(File.dirname(__FILE__), '../vendor/plugins/engines/boot')
17 rescue LoadError
18 # Not available
19 end
20
21 Rails::Initializer.run do |config|
22 # Settings in config/environments/* take precedence those specified here
23
24 # Skip frameworks you're not going to use
25 # config.frameworks -= [ :action_web_service, :action_mailer ]
26
27 # Add additional load paths for sweepers
28 config.autoload_paths += %W( #{RAILS_ROOT}/app/sweepers )
29
30 # Force all environments to use the same logger level
31 # (by default production uses :info, the others :debug)
32 # config.log_level = :debug
33
34 # Enable page/fragment caching by setting a file-based store
35 # (remember to create the caching directory and make it readable to the application)
36 # config.action_controller.cache_store = :file_store, "#{RAILS_ROOT}/tmp/cache"
37
38 # Activate observers that should always be running
39 # config.active_record.observers = :cacher, :garbage_collector
40 config.active_record.observers = :message_observer, :issue_observer, :journal_observer, :news_observer, :document_observer, :wiki_content_observer, :comment_observer
41
42 # Make Active Record use UTC-base instead of local time
43 # config.active_record.default_timezone = :utc
44
45 # Use Active Record's schema dumper instead of SQL when creating the test database
46 # (enables use of different database adapters for development and test environments)
47 # config.active_record.schema_format = :ruby
48
49 # Deliveries are disabled by default. Do NOT modify this section.
50 # Define your email configuration in configuration.yml instead.
51 # It will automatically turn deliveries on
52 config.action_mailer.perform_deliveries = false
53
54 # Load any local configuration that is kept out of source control
55 # (e.g. gems, patches).
56 if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
57 instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
58 end
59 end
@@ -1,16 +1,19
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2 RedmineApp::Application.configure do
3 # In the development environment your application's code is reloaded on
4 # every request. This slows down response time but is perfect for development
5 # since you don't have to restart the webserver when you make code changes.
6 config.cache_classes = false
2
7
3 # In the development environment your application's code is reloaded on
8 # Log error messages when you accidentally call methods on nil.
4 # every request. This slows down response time but is perfect for development
9 config.whiny_nils = true
5 # since you don't have to restart the webserver when you make code changes.
6 config.cache_classes = false
7
10
8 # Log error messages when you accidentally call methods on nil.
11 # Show full error reports and disable caching
9 config.whiny_nils = true
12 #config.action_controller.consider_all_requests_local = true
13 config.action_controller.perform_caching = false
10
14
11 # Show full error reports and disable caching
15 # Don't care if the mailer can't send
12 config.action_controller.consider_all_requests_local = true
16 config.action_mailer.raise_delivery_errors = false
13 config.action_controller.perform_caching = false
14
17
15 # Don't care if the mailer can't send
18 config.active_support.deprecation = :log
16 config.action_mailer.raise_delivery_errors = false
19 end
@@ -1,30 +1,32
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2 RedmineApp::Application.configure do
3 # The production environment is meant for finished, "live" apps.
4 # Code is not reloaded between requests
5 config.cache_classes = true
2
6
3 # The production environment is meant for finished, "live" apps.
7 #####
4 # Code is not reloaded between requests
8 # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html)
5 config.cache_classes = true
9 #
10 # Use a different logger for distributed setups
11 # config.logger = SyslogLogger.new
12 #
13 # Rotate logs bigger than 1MB, keeps no more than 7 rotated logs around.
14 # When setting a new Logger, make sure to set it's log level too.
15 #
16 # config.logger = Logger.new(config.log_path, 7, 1048576)
17 # config.logger.level = Logger::INFO
6
18
7 #####
19 # Full error reports are disabled and caching is turned on
8 # Customize the default logger (http://ruby-doc.org/core/classes/Logger.html)
20 config.action_controller.perform_caching = true
9 #
10 # Use a different logger for distributed setups
11 # config.logger = SyslogLogger.new
12 #
13 # Rotate logs bigger than 1MB, keeps no more than 7 rotated logs around.
14 # When setting a new Logger, make sure to set it's log level too.
15 #
16 # config.logger = Logger.new(config.log_path, 7, 1048576)
17 # config.logger.level = Logger::INFO
18
21
19 # Full error reports are disabled and caching is turned on
22 # Enable serving of images, stylesheets, and javascripts from an asset server
20 config.action_controller.consider_all_requests_local = false
23 # config.action_controller.asset_host = "http://assets.example.com"
21 config.action_controller.perform_caching = true
22
24
23 # Enable serving of images, stylesheets, and javascripts from an asset server
25 # Disable delivery errors if you bad email addresses should just be ignored
24 # config.action_controller.asset_host = "http://assets.example.com"
26 config.action_mailer.raise_delivery_errors = false
25
27
26 # Disable delivery errors if you bad email addresses should just be ignored
28 # No email in production log
27 config.action_mailer.raise_delivery_errors = false
29 config.action_mailer.logger = nil
28
30
29 # No email in production log
31 config.active_support.deprecation = :log
30 config.action_mailer.logger = nil
32 end
@@ -1,25 +1,25
1 # Settings specified here will take precedence over those in config/environment.rb
1 # Settings specified here will take precedence over those in config/environment.rb
2 RedmineApp::Application.configure do
3 # The test environment is used exclusively to run your application's
4 # test suite. You never need to work with it otherwise. Remember that
5 # your test database is "scratch space" for the test suite and is wiped
6 # and recreated between test runs. Don't rely on the data there!
7 config.cache_classes = true
2
8
3 # The test environment is used exclusively to run your application's
9 # Log error messages when you accidentally call methods on nil.
4 # test suite. You never need to work with it otherwise. Remember that
10 config.whiny_nils = true
5 # your test database is "scratch space" for the test suite and is wiped
6 # and recreated between test runs. Don't rely on the data there!
7 config.cache_classes = true
8
11
9 # Log error messages when you accidentally call methods on nil.
12 # Show full error reports and disable caching
10 config.whiny_nils = true
13 #config.action_controller.consider_all_requests_local = true
14 config.action_controller.perform_caching = false
11
15
12 # Show full error reports and disable caching
16 config.action_mailer.perform_deliveries = true
13 config.action_controller.consider_all_requests_local = true
17 config.action_mailer.delivery_method = :test
14 config.action_controller.perform_caching = false
15
18
16 config.action_mailer.perform_deliveries = true
19 # Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
17 config.action_mailer.delivery_method = :test
20 config.action_controller.allow_forgery_protection = false
18
21
19 config.action_controller.session = {
22 config.active_support.deprecation = :log
20 :key => "_test_session",
21 :secret => "some secret phrase for the tests."
22 }
23
23
24 # Skip protect_from_forgery in requests http://m.onkey.org/2007/9/28/csrf-protection-for-your-existing-rails-application
24 config.secret_token = 'a secret token for running the tests'
25 config.action_controller.allow_forgery_protection = false
25 end
@@ -8,6 +8,9 require 'active_record'
8 module ActiveRecord
8 module ActiveRecord
9 class Base
9 class Base
10 include Redmine::I18n
10 include Redmine::I18n
11 def self.named_scope(*args)
12 scope(*args)
13 end
11
14
12 # Translate attribute names for validation errors display
15 # Translate attribute names for validation errors display
13 def self.human_attribute_name(attr, *args)
16 def self.human_attribute_name(attr, *args)
@@ -35,6 +38,18 module ActionView
35 end
38 end
36 end
39 end
37 end
40 end
41
42 class Resolver
43 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
44 cached(key, [name, prefix, partial], details, locals) do
45 if details[:formats] & [:xml, :json]
46 details = details.dup
47 details[:formats] = details[:formats].dup + [:api]
48 end
49 find_templates(name, prefix, partial, details)
50 end
51 end
52 end
38 end
53 end
39
54
40 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
55 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
@@ -60,26 +75,9 end
60
75
61 ActionMailer::Base.send :include, AsynchronousMailer
76 ActionMailer::Base.send :include, AsynchronousMailer
62
77
63 module TMail
64 # TMail::Unquoter.convert_to_with_fallback_on_iso_8859_1 introduced in TMail 1.2.7
65 # triggers a test failure in test_add_issue_with_japanese_keywords(MailHandlerTest)
66 class Unquoter
67 class << self
68 alias_method :convert_to, :convert_to_without_fallback_on_iso_8859_1
69 end
70 end
71
72 # Patch for TMail 1.2.7. See http://www.redmine.org/issues/8751
73 class Encoder
74 def puts_meta(str)
75 add_text str
76 end
77 end
78 end
79
80 module ActionController
78 module ActionController
81 module MimeResponds
79 module MimeResponds
82 class Responder
80 class Collector
83 def api(&block)
81 def api(&block)
84 any(:xml, :json, &block)
82 any(:xml, :json, &block)
85 end
83 end
@@ -1,5 +1,4
1 # Add new mime types for use in respond_to blocks:
1 # Add new mime types for use in respond_to blocks:
2
2
3 Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
3 Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
4 Mime::Type.register 'application/pdf', :pdf
4
5 Mime::Type.register 'image/png', :png
@@ -3,3 +3,5 I18n.default_locale = 'en'
3 I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
3 I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
4
4
5 require 'redmine'
5 require 'redmine'
6
7 Redmine::Plugin.load
This diff has been collapsed as it changes many lines, (670 lines changed) Show them Hide them
@@ -1,398 +1,320
1 ActionController::Routing::Routes.draw do |map|
1 # Redmine - project management software
2 # Add your own custom routes here.
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 # The priority is based upon order of creation: first created -> highest priority.
3 #
4
4 # This program is free software; you can redistribute it and/or
5 # Here's a sample route:
5 # modify it under the terms of the GNU General Public License
6 # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
6 # as published by the Free Software Foundation; either version 2
7 # Keep in mind you can assign values other than :controller and :action
7 # of the License, or (at your option) any later version.
8
8 #
9 map.home '', :controller => 'welcome', :conditions => {:method => :get}
9 # This program is distributed in the hope that it will be useful,
10
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 map.signin 'login', :controller => 'account', :action => 'login',
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 :conditions => {:method => [:get, :post]}
12 # GNU General Public License for more details.
13 map.signout 'logout', :controller => 'account', :action => 'logout',
13 #
14 :conditions => {:method => :get}
14 # You should have received a copy of the GNU General Public License
15 map.connect 'account/register', :controller => 'account', :action => 'register',
15 # along with this program; if not, write to the Free Software
16 :conditions => {:method => [:get, :post]}
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 map.connect 'account/lost_password', :controller => 'account', :action => 'lost_password',
17
18 :conditions => {:method => [:get, :post]}
18 RedmineApp::Application.routes.draw do
19 map.connect 'account/activate', :controller => 'account', :action => 'activate',
19 root :to => 'welcome#index', :as => 'home'
20 :conditions => {:method => :get}
20
21
21 match 'login', :to => 'account#login', :as => 'signin'
22 map.connect 'projects/:id/wiki', :controller => 'wikis',
22 match 'logout', :to => 'account#logout', :as => 'signout'
23 :action => 'edit', :conditions => {:method => :post}
23 match 'account/register', :to => 'account#register', :via => [:get, :post]
24 map.connect 'projects/:id/wiki/destroy', :controller => 'wikis',
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post]
25 :action => 'destroy', :conditions => {:method => [:get, :post]}
25 match 'account/activate', :to => 'account#activate', :via => :get
26
26
27 map.with_options :controller => 'messages' do |messages_routes|
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
28 messages_routes.with_options :conditions => {:method => :get} do |messages_views|
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
29 messages_views.connect 'boards/:board_id/topics/new', :action => 'new'
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
30 messages_views.connect 'boards/:board_id/topics/:id', :action => 'show'
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
31 messages_views.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
31
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
36 get 'boards/:board_id/topics/:id', :to => 'messages#show'
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44
45 # Misc issue routes. TODO: move into resources
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53
54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
55 match '/issues/gantt', :to => 'gantts#show'
56
57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
58 match '/issues/calendar', :to => 'calendars#show'
59
60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
62
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74
75 resources :users
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87
88 match 'projects/:id/settings/:tab', :to => "projects#settings"
89
90 resources :projects do
91 member do
92 get 'settings'
93 post 'modules'
94 post 'archive'
95 post 'unarchive'
96 match 'copy', :via => [:get, :post]
32 end
97 end
33 messages_routes.with_options :conditions => {:method => :post} do |messages_actions|
98
34 messages_actions.connect 'boards/:board_id/topics/new', :action => 'new'
99 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :create, :update, :destroy] do
35 messages_actions.connect 'boards/:board_id/topics/preview', :action => 'preview'
100 collection do
36 messages_actions.connect 'boards/:board_id/topics/quote/:id', :action => 'quote'
101 get 'autocomplete'
37 messages_actions.connect 'boards/:board_id/topics/:id/replies', :action => 'reply'
102 end
38 messages_actions.connect 'boards/:board_id/topics/:id/edit', :action => 'edit'
39 messages_actions.connect 'boards/:board_id/topics/:id/destroy', :action => 'destroy'
40 end
103 end
41 end
42
104
43 # Misc issue routes. TODO: move into resources
105 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
44 map.auto_complete_issues '/issues/auto_complete', :controller => 'auto_completes',
45 :action => 'issues', :conditions => { :method => :get }
46 # TODO: would look nicer as /issues/:id/preview
47 map.preview_new_issue '/issues/preview/new/:project_id', :controller => 'previews',
48 :action => 'issue'
49 map.preview_edit_issue '/issues/preview/edit/:id', :controller => 'previews',
50 :action => 'issue'
51 map.issues_context_menu '/issues/context_menu',
52 :controller => 'context_menus', :action => 'issues'
53
54 map.issue_changes '/issues/changes', :controller => 'journals', :action => 'index'
55 map.quoted_issue '/issues/:id/quoted', :controller => 'journals', :action => 'new',
56 :id => /\d+/, :conditions => { :method => :post }
57
58 map.connect '/journals/diff/:id', :controller => 'journals', :action => 'diff',
59 :id => /\d+/, :conditions => { :method => :get }
60 map.connect '/journals/edit/:id', :controller => 'journals', :action => 'edit',
61 :id => /\d+/, :conditions => { :method => [:get, :post] }
62
63 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes|
64 gantts_routes.connect '/projects/:project_id/issues/gantt'
65 gantts_routes.connect '/projects/:project_id/issues/gantt.:format'
66 gantts_routes.connect '/issues/gantt.:format'
67 end
68
106
69 map.with_options :controller => 'calendars', :action => 'show' do |calendars_routes|
107 match 'issues/:copy_from/copy', :to => 'issues#new'
70 calendars_routes.connect '/projects/:project_id/issues/calendar'
108 resources :issues, :only => [:index, :new, :create] do
71 calendars_routes.connect '/issues/calendar'
109 resources :time_entries, :controller => 'timelog' do
72 end
110 collection do
111 get 'report'
112 end
113 end
114 end
115 # issue form update
116 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
73
117
74 map.with_options :controller => 'reports', :conditions => {:method => :get} do |reports|
118 resources :files, :only => [:index, :new, :create]
75 reports.connect 'projects/:id/issues/report', :action => 'issue_report'
76 reports.connect 'projects/:id/issues/report/:detail', :action => 'issue_report_details'
77 end
78
119
79 map.connect 'my/account', :controller => 'my', :action => 'account',
120 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
80 :conditions => {:method => [:get, :post]}
121 collection do
81 map.connect 'my/account/destroy', :controller => 'my', :action => 'destroy',
122 put 'close_completed'
82 :conditions => {:method => [:get, :post]}
123 end
83 map.connect 'my/page', :controller => 'my', :action => 'page',
124 end
84 :conditions => {:method => :get}
125 match 'versions.:format', :to => 'versions#index'
85 # Redirects to my/page
126 match 'roadmap', :to => 'versions#index', :format => false
86 map.connect 'my', :controller => 'my', :action => 'index',
127 match 'versions', :to => 'versions#index'
87 :conditions => {:method => :get}
88 map.connect 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key',
89 :conditions => {:method => :post}
90 map.connect 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key',
91 :conditions => {:method => :post}
92 map.connect 'my/password', :controller => 'my', :action => 'password',
93 :conditions => {:method => [:get, :post]}
94 map.connect 'my/page_layout', :controller => 'my', :action => 'page_layout',
95 :conditions => {:method => :get}
96 map.connect 'my/add_block', :controller => 'my', :action => 'add_block',
97 :conditions => {:method => :post}
98 map.connect 'my/remove_block', :controller => 'my', :action => 'remove_block',
99 :conditions => {:method => :post}
100 map.connect 'my/order_blocks', :controller => 'my', :action => 'order_blocks',
101 :conditions => {:method => :post}
102
103 map.with_options :controller => 'users' do |users|
104 users.user_membership 'users/:id/memberships/:membership_id',
105 :action => 'edit_membership',
106 :conditions => {:method => :put}
107 users.connect 'users/:id/memberships/:membership_id',
108 :action => 'destroy_membership',
109 :conditions => {:method => :delete}
110 users.user_memberships 'users/:id/memberships',
111 :action => 'edit_membership',
112 :conditions => {:method => :post}
113 end
114 map.resources :users
115
116 # For nice "roadmap" in the url for the index action
117 map.connect 'projects/:project_id/roadmap', :controller => 'versions', :action => 'index'
118
119 map.preview_news '/news/preview', :controller => 'previews', :action => 'news'
120 map.connect 'news/:id/comments', :controller => 'comments',
121 :action => 'create', :conditions => {:method => :post}
122 map.connect 'news/:id/comments/:comment_id', :controller => 'comments',
123 :action => 'destroy', :conditions => {:method => :delete}
124
125 map.connect 'watchers/new', :controller=> 'watchers', :action => 'new',
126 :conditions => {:method => :get}
127 map.connect 'watchers', :controller=> 'watchers', :action => 'create',
128 :conditions => {:method => :post}
129 map.connect 'watchers/append', :controller=> 'watchers', :action => 'append',
130 :conditions => {:method => :post}
131 map.connect 'watchers/destroy', :controller=> 'watchers', :action => 'destroy',
132 :conditions => {:method => :post}
133 map.connect 'watchers/watch', :controller=> 'watchers', :action => 'watch',
134 :conditions => {:method => :post}
135 map.connect 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch',
136 :conditions => {:method => :post}
137 map.connect 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user',
138 :conditions => {:method => :get}
139
128
140 # TODO: port to be part of the resources route(s)
129 resources :news, :except => [:show, :edit, :update, :destroy]
141 map.with_options :conditions => {:method => :get} do |project_views|
130 resources :time_entries, :controller => 'timelog' do
142 project_views.connect 'projects/:id/settings/:tab',
131 get 'report', :on => :collection
143 :controller => 'projects', :action => 'settings'
132 end
144 project_views.connect 'projects/:project_id/issues/:copy_from/copy',
133 resources :queries, :only => [:new, :create]
145 :controller => 'issues', :action => 'new'
134 resources :issue_categories, :shallow => true
146 end
135 resources :documents, :except => [:show, :edit, :update, :destroy]
136 resources :boards
137 resources :repositories, :shallow => true, :except => [:index, :show] do
138 member do
139 match 'committers', :via => [:get, :post]
140 end
141 end
147
142
148 map.resources :projects, :member => {
143 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
149 :copy => [:get, :post],
144 match 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
150 :settings => :get,
145 match 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff'
151 :modules => :post,
146 resources :wiki, :except => [:index, :new, :create] do
152 :archive => :post,
147 member do
153 :unarchive => :post
148 get 'rename'
154 } do |project|
149 post 'rename'
155 project.resource :enumerations, :controller => 'project_enumerations',
150 get 'history'
156 :only => [:update, :destroy]
151 get 'diff'
157 # issue form update
152 match 'preview', :via => [:post, :put]
158 project.issue_form 'issues/new', :controller => 'issues',
153 post 'protect'
159 :action => 'new', :conditions => {:method => [:post, :put]}
154 post 'add_attachment'
160 project.resources :issues, :only => [:index, :new, :create] do |issues|
155 end
161 issues.resources :time_entries, :controller => 'timelog',
156 collection do
162 :collection => {:report => :get}
157 get 'export'
158 get 'date_index'
159 end
163 end
160 end
161 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
162 match 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
163 end
164
164
165 project.resources :files, :only => [:index, :new, :create]
165 resources :issues do
166 project.resources :versions, :shallow => true,
166 collection do
167 :collection => {:close_completed => :put},
167 match 'bulk_edit', :via => [:get, :post]
168 :member => {:status_by => :post}
168 post 'bulk_update'
169 project.resources :news, :shallow => true
169 end
170 project.resources :time_entries, :controller => 'timelog',
170 resources :time_entries, :controller => 'timelog' do
171 :collection => {:report => :get}
171 collection do
172 project.resources :queries, :only => [:new, :create]
172 get 'report'
173 project.resources :issue_categories, :shallow => true
173 end
174 project.resources :documents, :shallow => true, :member => {:add_attachment => :post}
174 end
175 project.resources :boards
175 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
176 project.resources :repositories, :shallow => true, :except => [:index, :show],
177 :member => {:committers => [:get, :post]}
178 project.resources :memberships, :shallow => true, :controller => 'members',
179 :only => [:index, :show, :create, :update, :destroy],
180 :collection => {:autocomplete => :get}
181
182 project.wiki_start_page 'wiki', :controller => 'wiki', :action => 'show', :conditions => {:method => :get}
183 project.wiki_index 'wiki/index', :controller => 'wiki', :action => 'index', :conditions => {:method => :get}
184 project.wiki_diff 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff', :version => nil
185 project.wiki_diff 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
186 project.wiki_annotate 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
187 project.resources :wiki, :except => [:new, :create], :member => {
188 :rename => [:get, :post],
189 :history => :get,
190 :preview => :any,
191 :protect => :post,
192 :add_attachment => :post
193 }, :collection => {
194 :export => :get,
195 :date_index => :get
196 }
197 end
176 end
177 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
178
179 resources :queries, :except => [:show]
198
180
199 map.connect 'news', :controller => 'news', :action => 'index'
181 resources :news, :only => [:index, :show, :edit, :update, :destroy]
200 map.connect 'news.:format', :controller => 'news', :action => 'index'
182 match '/news/:id/comments', :to => 'comments#create', :via => :post
201
183 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
202 map.resources :queries, :except => [:show]
184
203 map.resources :issues,
185 resources :versions, :only => [:show, :edit, :update, :destroy] do
204 :collection => {:bulk_edit => [:get, :post], :bulk_update => :post} do |issues|
186 post 'status_by', :on => :member
205 issues.resources :time_entries, :controller => 'timelog',
206 :collection => {:report => :get}
207 issues.resources :relations, :shallow => true,
208 :controller => 'issue_relations',
209 :only => [:index, :show, :create, :destroy]
210 end
187 end
211 # Bulk deletion
188
212 map.connect '/issues', :controller => 'issues', :action => 'destroy',
189 resources :documents, :only => [:show, :edit, :update, :destroy] do
213 :conditions => {:method => :delete}
190 post 'add_attachment', :on => :member
214
215 map.connect '/time_entries/destroy',
216 :controller => 'timelog', :action => 'destroy',
217 :conditions => { :method => :delete }
218 map.time_entries_context_menu '/time_entries/context_menu',
219 :controller => 'context_menus', :action => 'time_entries'
220
221 map.resources :time_entries, :controller => 'timelog',
222 :collection => {:report => :get, :bulk_edit => :get, :bulk_update => :post}
223
224 map.with_options :controller => 'activities', :action => 'index',
225 :conditions => {:method => :get} do |activity|
226 activity.connect 'projects/:id/activity'
227 activity.connect 'projects/:id/activity.:format'
228 activity.connect 'activity', :id => nil
229 activity.connect 'activity.:format', :id => nil
230 end
191 end
231
192
232 map.with_options :controller => 'repositories' do |repositories|
193 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
233 repositories.with_options :conditions => {:method => :get} do |repository_views|
194
234 repository_views.connect 'projects/:id/repository',
195 resources :time_entries, :controller => 'timelog', :except => :destroy do
235 :action => 'show'
196 collection do
236
197 get 'report'
237 repository_views.connect 'projects/:id/repository/:repository_id/statistics',
198 get 'bulk_edit'
238 :action => 'stats'
199 post 'bulk_update'
239 repository_views.connect 'projects/:id/repository/:repository_id/graph',
240 :action => 'graph'
241
242 repository_views.connect 'projects/:id/repository/statistics',
243 :action => 'stats'
244 repository_views.connect 'projects/:id/repository/graph',
245 :action => 'graph'
246
247 repository_views.connect 'projects/:id/repository/:repository_id/revisions',
248 :action => 'revisions'
249 repository_views.connect 'projects/:id/repository/:repository_id/revisions.:format',
250 :action => 'revisions'
251 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev',
252 :action => 'revision'
253 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues',
254 :action => 'add_related_issue', :conditions => {:method => :post}
255 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id',
256 :action => 'remove_related_issue', :conditions => {:method => :delete}
257 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff',
258 :action => 'diff'
259 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/diff.:format',
260 :action => 'diff'
261 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/raw/*path',
262 :action => 'entry', :format => 'raw'
263 repository_views.connect 'projects/:id/repository/:repository_id/revisions/:rev/:action/*path',
264 :requirements => {
265 :action => /(browse|show|entry|changes|annotate|diff)/,
266 :rev => /[a-z0-9\.\-_]+/
267 }
268 repository_views.connect 'projects/:id/repository/:repository_id/raw/*path',
269 :action => 'entry', :format => 'raw'
270 repository_views.connect 'projects/:id/repository/:repository_id/:action/*path',
271 :requirements => { :action => /(browse|entry|changes|annotate|diff)/ }
272 repository_views.connect 'projects/:id/repository/:repository_id/show/*path',
273 :requirements => { :path => /.+/ }
274
275 repository_views.connect 'projects/:id/repository/:repository_id/revision',
276 :action => 'revision'
277
278 repository_views.connect 'projects/:id/repository/revisions',
279 :action => 'revisions'
280 repository_views.connect 'projects/:id/repository/revisions.:format',
281 :action => 'revisions'
282 repository_views.connect 'projects/:id/repository/revisions/:rev',
283 :action => 'revision'
284 repository_views.connect 'projects/:id/repository/revisions/:rev/issues',
285 :action => 'add_related_issue', :conditions => {:method => :post}
286 repository_views.connect 'projects/:id/repository/revisions/:rev/issues/:issue_id',
287 :action => 'remove_related_issue', :conditions => {:method => :delete}
288 repository_views.connect 'projects/:id/repository/revisions/:rev/diff',
289 :action => 'diff'
290 repository_views.connect 'projects/:id/repository/revisions/:rev/diff.:format',
291 :action => 'diff'
292 repository_views.connect 'projects/:id/repository/revisions/:rev/raw/*path',
293 :action => 'entry', :format => 'raw'
294 repository_views.connect 'projects/:id/repository/revisions/:rev/:action/*path',
295 :requirements => {
296 :action => /(browse|show|entry|changes|annotate|diff)/,
297 :rev => /[a-z0-9\.\-_]+/
298 }
299 repository_views.connect 'projects/:id/repository/raw/*path',
300 :action => 'entry', :format => 'raw'
301 repository_views.connect 'projects/:id/repository/:action/*path',
302 :requirements => { :action => /(browse|show|entry|changes|annotate|diff)/ }
303
304 repository_views.connect 'projects/:id/repository/revision',
305 :action => 'revision'
306
307 repository_views.connect 'projects/:id/repository/:repository_id',
308 :action => 'show'
309 end
200 end
310 end
201 end
202 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
203 # TODO: delete /time_entries for bulk deletion
204 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
205
206 # TODO: port to be part of the resources route(s)
207 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
208
209 get 'projects/:id/activity', :to => 'activities#index'
210 get 'projects/:id/activity.:format', :to => 'activities#index'
211 get 'activity', :to => 'activities#index'
212
213 # repositories routes
214 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
215 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
216 match 'projects/:id/repository/:repository_id/committers', :to => 'repositories#committers', :via => [:get, :post]
217
218 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
219 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
220 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
221 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
222 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
223 get 'projects/:id/repository/:repository_id/revisions/:rev/:format(/*path(.:ext))', :to => 'repositories#entry', :format => /raw/
224 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))', :controller => 'repositories', :action => /(browse|show|entry|changes|annotate|diff)/
225
226 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
227 get 'projects/:id/repository/graph', :to => 'repositories#graph'
228 match 'projects/:id/repository/committers', :to => 'repositories#committers', :via => [:get, :post]
229
230 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
231 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
232 get 'projects/:id/repository/revision', :to => 'repositories#revision'
233 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
234 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
235 get 'projects/:id/repository/revisions/:rev/:format(/*path(.:ext))', :to => 'repositories#entry', :format => /raw/
236 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))', :controller => 'repositories', :action => /(browse|show|entry|changes|annotate|diff)/
237 get 'projects/:id/repository/:repository_id/:format(/*path(.:ext))', :to => 'repositories#entry', :format => /raw/
238 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))', :controller => 'repositories', :action => /(browse|show|entry|changes|annotate|diff)/
239 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
240
241 get 'projects/:id/repository/:format(/*path(.:ext))', :to => 'repositories#entry', :format => /raw/
242 get 'projects/:id/repository/:action(/*path(.:ext))', :controller => 'repositories', :action => /(browse|show|entry|changes|annotate|diff)/
243 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
311
244
312 # additional routes for having the file name at the end of url
245 # additional routes for having the file name at the end of url
313 map.connect 'attachments/:id/:filename', :controller => 'attachments',
246 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
314 :action => 'show', :id => /\d+/, :filename => /.*/,
247 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
315 :conditions => {:method => :get}
248 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
316 map.connect 'attachments/download/:id/:filename', :controller => 'attachments',
249 resources :attachments, :only => [:show, :destroy]
317 :action => 'download', :id => /\d+/, :filename => /.*/,
250
318 :conditions => {:method => :get}
251 resources :groups do
319 map.connect 'attachments/download/:id', :controller => 'attachments',
252 member do
320 :action => 'download', :id => /\d+/,
253 get 'autocomplete_for_user'
321 :conditions => {:method => :get}
254 end
322 map.resources :attachments, :only => [:show, :destroy]
255 end
323
256
324 map.resources :groups, :member => {:autocomplete_for_user => :get}
257 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
325 map.group_users 'groups/:id/users', :controller => 'groups',
258 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
326 :action => 'add_users', :id => /\d+/,
259 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
327 :conditions => {:method => :post}
260 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
328 map.group_user 'groups/:id/users/:user_id', :controller => 'groups',
261
329 :action => 'remove_user', :id => /\d+/,
262 resources :trackers, :except => :show
330 :conditions => {:method => :delete}
263 resources :issue_statuses, :except => :show do
331 map.connect 'groups/destroy_membership/:id', :controller => 'groups',
264 collection do
332 :action => 'destroy_membership', :id => /\d+/,
265 post 'update_issue_done_ratio'
333 :conditions => {:method => :post}
266 end
334 map.connect 'groups/edit_membership/:id', :controller => 'groups',
267 end
335 :action => 'edit_membership', :id => /\d+/,
268 resources :custom_fields, :except => :show
336 :conditions => {:method => :post}
269 resources :roles, :except => :show do
337
270 collection do
338 map.resources :trackers, :except => :show
271 match 'permissions', :via => [:get, :post]
339 map.resources :issue_statuses, :except => :show, :collection => {:update_issue_done_ratio => :post}
272 end
340 map.resources :custom_fields, :except => :show
341 map.resources :roles, :except => :show, :collection => {:permissions => [:get, :post]}
342 map.resources :enumerations, :except => :show
343
344 map.connect 'projects/:id/search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
345 map.connect 'search', :controller => 'search', :action => 'index', :conditions => {:method => :get}
346
347 map.connect 'mail_handler', :controller => 'mail_handler',
348 :action => 'index', :conditions => {:method => :post}
349
350 map.connect 'admin', :controller => 'admin', :action => 'index',
351 :conditions => {:method => :get}
352 map.connect 'admin/projects', :controller => 'admin', :action => 'projects',
353 :conditions => {:method => :get}
354 map.connect 'admin/plugins', :controller => 'admin', :action => 'plugins',
355 :conditions => {:method => :get}
356 map.connect 'admin/info', :controller => 'admin', :action => 'info',
357 :conditions => {:method => :get}
358 map.connect 'admin/test_email', :controller => 'admin', :action => 'test_email',
359 :conditions => {:method => :get}
360 map.connect 'admin/default_configuration', :controller => 'admin',
361 :action => 'default_configuration', :conditions => {:method => :post}
362
363 map.resources :auth_sources, :member => {:test_connection => :get}
364
365 map.connect 'workflows', :controller => 'workflows',
366 :action => 'index', :conditions => {:method => :get}
367 map.connect 'workflows/edit', :controller => 'workflows',
368 :action => 'edit', :conditions => {:method => [:get, :post]}
369 map.connect 'workflows/copy', :controller => 'workflows',
370 :action => 'copy', :conditions => {:method => [:get, :post]}
371
372 map.connect 'settings', :controller => 'settings',
373 :action => 'index', :conditions => {:method => :get}
374 map.connect 'settings/edit', :controller => 'settings',
375 :action => 'edit', :conditions => {:method => [:get, :post]}
376 map.connect 'settings/plugin/:id', :controller => 'settings',
377 :action => 'plugin', :conditions => {:method => [:get, :post]}
378
379 map.with_options :controller => 'sys' do |sys|
380 sys.connect 'sys/projects.:format',
381 :action => 'projects',
382 :conditions => {:method => :get}
383 sys.connect 'sys/projects/:id/repository.:format',
384 :action => 'create_project_repository',
385 :conditions => {:method => :post}
386 sys.connect 'sys/fetch_changesets',
387 :action => 'fetch_changesets',
388 :conditions => {:method => :get}
389 end
273 end
274 resources :enumerations, :except => :show
390
275
391 map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post}
276 get 'projects/:id/search', :controller => 'search', :action => 'index'
277 get 'search', :controller => 'search', :action => 'index'
392
278
393 map.connect 'robots.txt', :controller => 'welcome',
279 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
394 :action => 'robots', :conditions => {:method => :get}
395
280
396 # Used for OpenID
281 match 'admin', :controller => 'admin', :action => 'index', :via => :get
397 map.root :controller => 'account', :action => 'login'
282 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
283 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
284 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
285 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
286 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
287
288 resources :auth_sources do
289 member do
290 get 'test_connection'
291 end
292 end
293
294 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
295 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
296 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
297 match 'settings', :controller => 'settings', :action => 'index', :via => :get
298 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
299 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
300
301 match 'sys/projects', :to => 'sys#projects', :via => :get
302 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
303 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
304
305 match 'uploads', :to => 'attachments#upload', :via => :post
306
307 get 'robots.txt', :to => 'welcome#robots'
308
309 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
310 file = File.join(plugin_dir, "config/routes.rb")
311 if File.exists?(file)
312 begin
313 instance_eval File.read(file)
314 rescue Exception => e
315 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
316 exit 1
317 end
318 end
319 end
398 end
320 end
@@ -4,10 +4,11 This is a sample plugin for Redmine
4
4
5 == Installation
5 == Installation
6
6
7 1. Copy the plugin directory into the vendor/plugins directory
7 1. Copy the plugin directory into the "plugins" directory at the root
8 of your Redmine directory
8
9
9 2. Migrate plugin:
10 2. Migrate and copy plugin assets:
10 rake db:migrate_plugins
11 rake redmine:plugins
11
12
12 3. Start Redmine
13 3. Start Redmine
13
14
@@ -1,5 +1,7
1 <h2>Good Bye</h2>
2
1 <p class="icon icon-example-works"><%= l(:text_say_goodbye) %></p>
3 <p class="icon icon-example-works"><%= l(:text_say_goodbye) %></p>
2
4
3 <% content_for :header_tags do %>
5 <% content_for :header_tags do %>
4 <%= stylesheet_link_tag "example.css", :plugin => "sample_plugin", :media => "screen" %>
6 <%= stylesheet_link_tag "/plugin_assets/sample_plugin/stylesheets/example.css", :media => "screen" %>
5 <% end %>
7 <% end %>
@@ -1,9 +1,15
1 <h2>Hello</h2>
2
1 <p class="icon icon-example-works"><%= l(:text_say_hello) %></p>
3 <p class="icon icon-example-works"><%= l(:text_say_hello) %></p>
2
4
3 <p><label>Example setting</label>: <%= @value %></p>
5 <p><label>Example setting</label>: <%= @value %></p>
4
6
5 <%= link_to_if_authorized 'Good bye', :action => 'say_goodbye', :id => @project %>
7 <%= link_to('Good bye', :action => 'say_goodbye', :id => @project) if User.current.allowed_to?(:example_say_goodbye, @project) %>
8
9 <% content_for :sidebar do %>
10 <p>Adding content to the sidebar...</p>
11 <% end %>
6
12
7 <% content_for :header_tags do %>
13 <% content_for :header_tags do %>
8 <%= stylesheet_link_tag "example.css", :plugin => "sample_plugin", :media => "screen" %>
14 <%= stylesheet_link_tag "/plugin_assets/sample_plugin/stylesheets/example.css", :media => "screen" %>
9 <% end %>
15 <% end %>
@@ -1,7 +1,4
1 # Redmine sample plugin
1 Rails.logger.info 'Starting Example plugin for RedMine'
2 require 'redmine'
3
4 RAILS_DEFAULT_LOGGER.info 'Starting Example plugin for RedMine'
5
2
6 Redmine::Plugin.register :sample_plugin do
3 Redmine::Plugin.register :sample_plugin do
7 name 'Example plugin'
4 name 'Example plugin'
@@ -3,19 +3,8 Description:
3
3
4 Example:
4 Example:
5 ./script/generate redmine_plugin meetings
5 ./script/generate redmine_plugin meetings
6 create vendor/plugins/redmine_meetings/app/controllers
6 create plugins/meetings/README.rdoc
7 create vendor/plugins/redmine_meetings/app/helpers
7 create plugins/meetings/init.rb
8 create vendor/plugins/redmine_meetings/app/models
8 create plugins/meetings/config/routes.rb
9 create vendor/plugins/redmine_meetings/app/views
9 create plugins/meetings/config/locales/en.yml
10 create vendor/plugins/redmine_meetings/db/migrate
10 create plugins/meetings/test/test_helper.rb
11 create vendor/plugins/redmine_meetings/lib/tasks
12 create vendor/plugins/redmine_meetings/assets/images
13 create vendor/plugins/redmine_meetings/assets/javascripts
14 create vendor/plugins/redmine_meetings/assets/stylesheets
15 create vendor/plugins/redmine_meetings/lang
16 create vendor/plugins/redmine_meetings/config/locales
17 create vendor/plugins/redmine_meetings/test
18 create vendor/plugins/redmine_meetings/README.rdoc
19 create vendor/plugins/redmine_meetings/init.rb
20 create vendor/plugins/redmine_meetings/config/locales/en.yml
21 create vendor/plugins/redmine_meetings/test/test_helper.rb
@@ -1,32 +1,33
1 class RedminePluginGenerator < Rails::Generator::NamedBase
1 class RedminePluginGenerator < Rails::Generators::NamedBase
2 source_root File.expand_path("../templates", __FILE__)
3
2 attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
4 attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
3
5
4 def initialize(runtime_args, runtime_options = {})
6 def initialize(*args)
5 super
7 super
6 @plugin_name = "redmine_#{file_name.underscore}"
8 @plugin_name = file_name.underscore
7 @plugin_pretty_name = plugin_name.titleize
9 @plugin_pretty_name = plugin_name.titleize
8 @plugin_path = "vendor/plugins/#{plugin_name}"
10 @plugin_path = "plugins/#{plugin_name}"
9 end
11 end
10
12
11 def manifest
13 def copy_templates
12 record do |m|
14 empty_directory "#{plugin_path}/app"
13 m.directory "#{plugin_path}/app/controllers"
15 empty_directory "#{plugin_path}/app/controllers"
14 m.directory "#{plugin_path}/app/helpers"
16 empty_directory "#{plugin_path}/app/helpers"
15 m.directory "#{plugin_path}/app/models"
17 empty_directory "#{plugin_path}/app/models"
16 m.directory "#{plugin_path}/app/views"
18 empty_directory "#{plugin_path}/app/views"
17 m.directory "#{plugin_path}/db/migrate"
19 empty_directory "#{plugin_path}/db/migrate"
18 m.directory "#{plugin_path}/lib/tasks"
20 empty_directory "#{plugin_path}/lib/tasks"
19 m.directory "#{plugin_path}/assets/images"
21 empty_directory "#{plugin_path}/assets/images"
20 m.directory "#{plugin_path}/assets/javascripts"
22 empty_directory "#{plugin_path}/assets/javascripts"
21 m.directory "#{plugin_path}/assets/stylesheets"
23 empty_directory "#{plugin_path}/assets/stylesheets"
22 m.directory "#{plugin_path}/lang"
24 empty_directory "#{plugin_path}/config/locales"
23 m.directory "#{plugin_path}/config/locales"
25 empty_directory "#{plugin_path}/test"
24 m.directory "#{plugin_path}/test"
25
26
26 m.template 'README.rdoc', "#{plugin_path}/README.rdoc"
27 template 'README.rdoc', "#{plugin_path}/README.rdoc"
27 m.template 'init.rb.erb', "#{plugin_path}/init.rb"
28 template 'init.rb.erb', "#{plugin_path}/init.rb"
28 m.template 'en_rails_i18n.yml', "#{plugin_path}/config/locales/en.yml"
29 template 'routes.rb', "#{plugin_path}/config/routes.rb"
29 m.template 'test_helper.rb.erb', "#{plugin_path}/test/test_helper.rb"
30 template 'en_rails_i18n.yml', "#{plugin_path}/config/locales/en.yml"
30 end
31 template 'test_helper.rb.erb', "#{plugin_path}/test/test_helper.rb"
31 end
32 end
32 end
33 end
@@ -1,5 +1,3
1 require 'redmine'
2
3 Redmine::Plugin.register :<%= plugin_name %> do
1 Redmine::Plugin.register :<%= plugin_name %> do
4 name '<%= plugin_pretty_name %> plugin'
2 name '<%= plugin_pretty_name %> plugin'
5 author 'Author name'
3 author 'Author name'
@@ -2,4 +2,4 Description:
2 Generates a plugin controller.
2 Generates a plugin controller.
3
3
4 Example:
4 Example:
5 ./script/generate redmine_plugin_controller MyPlugin Pools index show vote
5 ./script/generate redmine_plugin_controller meetings pools index show vote
@@ -1,55 +1,27
1 require 'rails_generator/base'
1 class RedminePluginControllerGenerator < Rails::Generators::NamedBase
2 require 'rails_generator/generators/components/controller/controller_generator'
2 source_root File.expand_path("../templates", __FILE__)
3 argument :controller, :type => :string
4 argument :actions, :type => :array, :default => [], :banner => "ACTION ACTION ..."
3
5
4 class RedminePluginControllerGenerator < ControllerGenerator
5 attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
6 attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
6
7
7 def initialize(runtime_args, runtime_options = {})
8 def initialize(*args)
8 runtime_args = runtime_args.dup
9 super
9 usage if runtime_args.empty?
10 @plugin_name = file_name.underscore
10 @plugin_name = "redmine_" + runtime_args.shift.underscore
11 @plugin_pretty_name = plugin_name.titleize
11 @plugin_pretty_name = plugin_name.titleize
12 @plugin_path = "vendor/plugins/#{plugin_name}"
12 @plugin_path = "plugins/#{plugin_name}"
13 super(runtime_args, runtime_options)
13 @controller_class = controller.camelize
14 end
14 end
15
15
16 def destination_root
16 def copy_templates
17 File.join(Rails.root, plugin_path)
17 template 'controller.rb.erb', "#{plugin_path}/app/controllers/#{controller}_controller.rb"
18 end
18 template 'helper.rb.erb', "#{plugin_path}/app/helpers/#{controller}_helper.rb"
19
19 template 'functional_test.rb.erb', "#{plugin_path}/test/functional/#{controller}_controller_test.rb"
20 def manifest
20 # View template for each action.
21 record do |m|
21 actions.each do |action|
22 # Check for class naming collisions.
22 path = "#{plugin_path}/app/views/#{controller}/#{action}.html.erb"
23 m.class_collisions class_path, "#{class_name}Controller", "#{class_name}ControllerTest", "#{class_name}Helper"
23 @action_name = action
24
24 template 'view.html.erb', path
25 # Controller, helper, views, and test directories.
26 m.directory File.join('app/controllers', class_path)
27 m.directory File.join('app/helpers', class_path)
28 m.directory File.join('app/views', class_path, file_name)
29 m.directory File.join('test/functional', class_path)
30
31 # Controller class, functional test, and helper class.
32 m.template 'controller.rb.erb',
33 File.join('app/controllers',
34 class_path,
35 "#{file_name}_controller.rb")
36
37 m.template 'functional_test.rb.erb',
38 File.join('test/functional',
39 class_path,
40 "#{file_name}_controller_test.rb")
41
42 m.template 'helper.rb.erb',
43 File.join('app/helpers',
44 class_path,
45 "#{file_name}_helper.rb")
46
47 # View template for each action.
48 actions.each do |action|
49 path = File.join('app/views', class_path, file_name, "#{action}.html.erb")
50 m.template 'view.html.erb', path,
51 :assigns => { :action => action, :path => path }
52 end
53 end
25 end
54 end
26 end
55 end
27 end
@@ -1,4 +1,4
1 class <%= class_name %>Controller < ApplicationController
1 class <%= @controller_class %>Controller < ApplicationController
2 unloadable
2 unloadable
3
3
4 <% actions.each do |action| -%>
4 <% actions.each do |action| -%>
@@ -1,6 +1,6
1 require File.dirname(__FILE__) + '/../test_helper'
1 require File.dirname(__FILE__) + '/../test_helper'
2
2
3 class <%= class_name %>ControllerTest < ActionController::TestCase
3 class <%= @controller_class %>ControllerTest < ActionController::TestCase
4 # Replace this with your real tests.
4 # Replace this with your real tests.
5 def test_truth
5 def test_truth
6 assert true
6 assert true
@@ -1,2 +1,2
1 module <%= class_name %>Helper
1 module <%= @controller_class %>Helper
2 end
2 end
@@ -1,1 +1,1
1 <h2><%= class_name %>#<%= action %></h2>
1 <h2><%= @controller_class %>Controller#<%= @action_name %></h2>
@@ -2,4 +2,4 Description:
2 Generates a plugin model.
2 Generates a plugin model.
3
3
4 Examples:
4 Examples:
5 ./script/generate redmine_plugin_model MyPlugin pool title:string question:text
5 ./script/generate redmine_plugin_model meetings pool
@@ -1,45 +1,19
1 require 'rails_generator/base'
1 class RedminePluginModelGenerator < Rails::Generators::NamedBase
2 require 'rails_generator/generators/components/model/model_generator'
2 source_root File.expand_path("../templates", __FILE__)
3 argument :model, :type => :string
3
4
4 class RedminePluginModelGenerator < ModelGenerator
5 attr_reader :plugin_path, :plugin_name, :plugin_pretty_name
5 attr_accessor :plugin_path, :plugin_name, :plugin_pretty_name
6
6
7 def initialize(runtime_args, runtime_options = {})
7 def initialize(*args)
8 runtime_args = runtime_args.dup
8 super
9 usage if runtime_args.empty?
9 @plugin_name = file_name.underscore
10 @plugin_name = "redmine_" + runtime_args.shift.underscore
11 @plugin_pretty_name = plugin_name.titleize
10 @plugin_pretty_name = plugin_name.titleize
12 @plugin_path = "vendor/plugins/#{plugin_name}"
11 @plugin_path = "plugins/#{plugin_name}"
13 super(runtime_args, runtime_options)
12 @model_class = model.camelize
14 end
13 end
15
14
16 def destination_root
15 def copy_templates
17 File.join(Rails.root, plugin_path)
16 template 'model.rb.erb', "#{plugin_path}/app/models/#{model}.rb"
18 end
17 template 'unit_test.rb.erb', "#{plugin_path}/test/unit/#{model}_test.rb"
19
20 def manifest
21 record do |m|
22 # Check for class naming collisions.
23 m.class_collisions class_path, class_name, "#{class_name}Test"
24
25 # Model, test, and fixture directories.
26 m.directory File.join('app/models', class_path)
27 m.directory File.join('test/unit', class_path)
28 m.directory File.join('test/fixtures', class_path)
29
30 # Model class, unit test, and fixtures.
31 m.template 'model.rb.erb', File.join('app/models', class_path, "#{file_name}.rb")
32 m.template 'unit_test.rb.erb', File.join('test/unit', class_path, "#{file_name}_test.rb")
33
34 unless options[:skip_fixture]
35 m.template 'fixtures.yml', File.join('test/fixtures', "#{table_name}.yml")
36 end
37
38 unless options[:skip_migration]
39 m.migration_template 'migration.rb.erb', 'db/migrate', :assigns => {
40 :migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}"
41 }, :migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
42 end
43 end
44 end
18 end
45 end
19 end
@@ -1,3 +1,3
1 class <%= class_name %> < ActiveRecord::Base
1 class <%= @model_class %> < ActiveRecord::Base
2 unloadable
2 unloadable
3 end
3 end
@@ -1,7 +1,6
1 require File.dirname(__FILE__) + '/../test_helper'
1 require File.dirname(__FILE__) + '/../test_helper'
2
2
3 class <%= class_name %>Test < ActiveSupport::TestCase
3 class <%= @model_class %>Test < ActiveSupport::TestCase
4 fixtures :<%= table_name %>
5
4
6 # Replace this with your real tests.
5 # Replace this with your real tests.
7 def test_truth
6 def test_truth
@@ -205,7 +205,7 Redmine::MenuManager.map :project_menu do |menu|
205 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
205 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
206 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
206 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
207 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
207 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
208 menu.push :repository, { :controller => 'repositories', :action => 'show' },
208 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
209 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
209 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
210 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
210 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
211 end
211 end
@@ -72,7 +72,7 module Redmine
72 all.each do |object|
72 all.each do |object|
73 clear = object.send(attribute)
73 clear = object.send(attribute)
74 object.send "#{attribute}=", clear
74 object.send "#{attribute}=", clear
75 raise(ActiveRecord::Rollback) unless object.save(false)
75 raise(ActiveRecord::Rollback) unless object.save(:validation => false)
76 end
76 end
77 end ? true : false
77 end ? true : false
78 end
78 end
@@ -81,8 +81,8 module Redmine
81 transaction do
81 transaction do
82 all.each do |object|
82 all.each do |object|
83 clear = object.send(attribute)
83 clear = object.send(attribute)
84 object.write_attribute attribute, clear
84 object.send :write_attribute, attribute, clear
85 raise(ActiveRecord::Rollback) unless object.save(false)
85 raise(ActiveRecord::Rollback) unless object.save(:validation => false)
86 end
86 end
87 end
87 end
88 end ? true : false
88 end ? true : false
@@ -16,38 +16,25
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module ActiveRecord
18 module ActiveRecord
19 class Base
19 module FinderMethods
20 def self.find_ids(options={})
20 def find_ids(*args)
21 find_ids_with_associations(options)
21 find_ids_with_associations
22 end
22 end
23 end
24
23
25 module Associations
24 private
26 module ClassMethods
25
27 def find_ids_with_associations(options = {})
26 def find_ids_with_associations
28 catch :invalid_query do
27 join_dependency = construct_join_dependency_for_association_find
29 join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
28 relation = construct_relation_for_association_find_ids(join_dependency)
30 return connection.select_values(construct_ids_finder_sql_with_included_associations(options, join_dependency)).map(&:to_i)
29 rows = connection.select_all(relation, 'SQL', relation.bind_values)
31 end
30 rows.map {|row| row["id"].to_i}
31 rescue ThrowResult
32 []
32 []
33 end
33 end
34
35 def construct_ids_finder_sql_with_included_associations(options, join_dependency)
36 scope = scope(:find)
37 sql = "SELECT #{table_name}.id FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
38 sql << join_dependency.join_associations.collect{|join| join.association_join }.join
39
40 add_joins!(sql, options[:joins], scope)
41 add_conditions!(sql, options[:conditions], scope)
42 add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
43
44 add_group!(sql, options[:group], options[:having], scope)
45 add_order!(sql, options[:order], scope)
46 add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
47 add_lock!(sql, options, scope)
48
34
49 return sanitize_sql(sql)
35 def construct_relation_for_association_find_ids(join_dependency)
50 end
36 relation = except(:includes, :eager_load, :preload, :select).select("#{table_name}.id")
37 apply_join_dependency(relation, join_dependency)
51 end
38 end
52 end
39 end
53 end
40 end
@@ -4,4 +4,8 require File.dirname(__FILE__) + '/string/inflections'
4 class String #:nodoc:
4 class String #:nodoc:
5 include Redmine::CoreExtensions::String::Conversions
5 include Redmine::CoreExtensions::String::Conversions
6 include Redmine::CoreExtensions::String::Inflections
6 include Redmine::CoreExtensions::String::Inflections
7
8 def is_binary_data?
9 ( self.count( "^ -~", "^\r\n" ).fdiv(self.size) > 0.3 || self.index( "\x00" ) ) unless empty?
10 end
7 end
11 end
@@ -17,7 +17,7
17
17
18 module Redmine
18 module Redmine
19 module Hook
19 module Hook
20 include ActionController::UrlWriter
20 #include ActionController::UrlWriter
21
21
22 @@listener_classes = []
22 @@listener_classes = []
23 @@listeners = nil
23 @@listeners = nil
@@ -88,12 +88,12 module Redmine
88 include ActionView::Helpers::FormTagHelper
88 include ActionView::Helpers::FormTagHelper
89 include ActionView::Helpers::FormOptionsHelper
89 include ActionView::Helpers::FormOptionsHelper
90 include ActionView::Helpers::JavaScriptHelper
90 include ActionView::Helpers::JavaScriptHelper
91 include ActionView::Helpers::PrototypeHelper
91 #include ActionView::Helpers::PrototypeHelper
92 include ActionView::Helpers::NumberHelper
92 include ActionView::Helpers::NumberHelper
93 include ActionView::Helpers::UrlHelper
93 include ActionView::Helpers::UrlHelper
94 include ActionView::Helpers::AssetTagHelper
94 include ActionView::Helpers::AssetTagHelper
95 include ActionView::Helpers::TextHelper
95 include ActionView::Helpers::TextHelper
96 include ActionController::UrlWriter
96 include Rails.application.routes.url_helpers
97 include ApplicationHelper
97 include ApplicationHelper
98
98
99 # Default to creating links using only the path. Subclasses can
99 # Default to creating links using only the path. Subclasses can
@@ -113,6 +113,14 module Redmine
113 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
113 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
114 end
114 end
115 end
115 end
116
117 def controller
118 nil
119 end
120
121 def config
122 ActionController::Base.config
123 end
116 end
124 end
117
125
118 # Helper module included in ApplicationHelper and ActionController so that
126 # Helper module included in ApplicationHelper and ActionController so that
@@ -143,7 +151,7 module Redmine
143 default_context = { :project => @project }
151 default_context = { :project => @project }
144 default_context[:controller] = controller if respond_to?(:controller)
152 default_context[:controller] = controller if respond_to?(:controller)
145 default_context[:request] = request if respond_to?(:request)
153 default_context[:request] = request if respond_to?(:request)
146 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
154 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
147 end
155 end
148 end
156 end
149 end
157 end
@@ -43,6 +43,9 module Redmine #:nodoc:
43 #
43 #
44 # When rendered, the plugin settings value is available as the local variable +settings+
44 # When rendered, the plugin settings value is available as the local variable +settings+
45 class Plugin
45 class Plugin
46 cattr_accessor :directory
47 self.directory = File.join(Rails.root, 'plugins')
48
46 cattr_accessor :public_directory
49 cattr_accessor :public_directory
47 self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
50 self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
48
51
@@ -70,9 +73,22 module Redmine #:nodoc:
70 p.instance_eval(&block)
73 p.instance_eval(&block)
71 # Set a default name if it was not provided during registration
74 # Set a default name if it was not provided during registration
72 p.name(id.to_s.humanize) if p.name.nil?
75 p.name(id.to_s.humanize) if p.name.nil?
76
73 # Adds plugin locales if any
77 # Adds plugin locales if any
74 # YAML translation files should be found under <plugin>/config/locales/
78 # YAML translation files should be found under <plugin>/config/locales/
75 ::I18n.load_path += Dir.glob(File.join(Rails.root, 'vendor', 'plugins', id.to_s, 'config', 'locales', '*.yml'))
79 ::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
80
81 # Prepends the app/views directory of the plugin to the view path
82 view_path = File.join(p.directory, 'app', 'views')
83 if File.directory?(view_path)
84 ActionController::Base.prepend_view_path(view_path)
85 end
86
87 # Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
88 Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
89 ActiveSupport::Dependencies.autoload_paths += [dir]
90 end
91
76 registered_plugins[id] = p
92 registered_plugins[id] = p
77 end
93 end
78
94
@@ -100,10 +116,38 module Redmine #:nodoc:
100 registered_plugins[id.to_sym].present?
116 registered_plugins[id.to_sym].present?
101 end
117 end
102
118
119 def self.load
120 Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
121 if File.directory?(directory)
122 lib = File.join(directory, "lib")
123 if File.directory?(lib)
124 $:.unshift lib
125 ActiveSupport::Dependencies.autoload_paths += [lib]
126 end
127 initializer = File.join(directory, "init.rb")
128 if File.file?(initializer)
129 require initializer
130 end
131 end
132 end
133 end
134
103 def initialize(id)
135 def initialize(id)
104 @id = id.to_sym
136 @id = id.to_sym
105 end
137 end
106
138
139 def directory
140 File.join(self.class.directory, id.to_s)
141 end
142
143 def public_directory
144 File.join(self.class.public_directory, id.to_s)
145 end
146
147 def assets_directory
148 File.join(directory, 'assets')
149 end
150
107 def <=>(plugin)
151 def <=>(plugin)
108 self.id.to_s <=> plugin.id.to_s
152 self.id.to_s <=> plugin.id.to_s
109 end
153 end
@@ -277,5 +321,96 module Redmine #:nodoc:
277 def configurable?
321 def configurable?
278 settings && settings.is_a?(Hash) && !settings[:partial].blank?
322 settings && settings.is_a?(Hash) && !settings[:partial].blank?
279 end
323 end
324
325 def mirror_assets
326 source = assets_directory
327 destination = public_directory
328 return unless File.directory?(source)
329
330 source_files = Dir[source + "/**/*"]
331 source_dirs = source_files.select { |d| File.directory?(d) }
332 source_files -= source_dirs
333
334 unless source_files.empty?
335 base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
336 FileUtils.mkdir_p(base_target_dir)
337 end
338
339 source_dirs.each do |dir|
340 # strip down these paths so we have simple, relative paths we can
341 # add to the destination
342 target_dir = File.join(destination, dir.gsub(source, ''))
343 begin
344 FileUtils.mkdir_p(target_dir)
345 rescue Exception => e
346 raise "Could not create directory #{target_dir}: \n" + e
347 end
348 end
349
350 source_files.each do |file|
351 begin
352 target = File.join(destination, file.gsub(source, ''))
353 unless File.exist?(target) && FileUtils.identical?(file, target)
354 FileUtils.cp(file, target)
355 end
356 rescue Exception => e
357 raise "Could not copy #{file} to #{target}: \n" + e
358 end
359 end
360 end
361
362 # The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
363 def migration_directory
364 File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
365 end
366
367 # Returns the version number of the latest migration for this plugin. Returns
368 # nil if this plugin has no migrations.
369 def latest_migration
370 migrations.last
371 end
372
373 # Returns the version numbers of all migrations for this plugin.
374 def migrations
375 migrations = Dir[migration_directory+"/*.rb"]
376 migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
377 end
378
379 # Migrate this plugin to the given version
380 def migrate(version = nil)
381 Redmine::Plugin::Migrator.migrate_plugin(self, version)
382 end
383
384 class Migrator < ActiveRecord::Migrator
385 # We need to be able to set the 'current' plugin being migrated.
386 cattr_accessor :current_plugin
387
388 class << self
389 # Runs the migrations from a plugin, up (or down) to the version given
390 def migrate_plugin(plugin, version)
391 self.current_plugin = plugin
392 return if current_version(plugin) == version
393 migrate(plugin.migration_directory, version)
394 end
395
396 def current_version(plugin=current_plugin)
397 # Delete migrations that don't match .. to_i will work because the number comes first
398 ::ActiveRecord::Base.connection.select_values(
399 "SELECT version FROM #{schema_migrations_table_name}"
400 ).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
401 end
402 end
403
404 def migrated
405 sm_table = self.class.schema_migrations_table_name
406 ::ActiveRecord::Base.connection.select_values(
407 "SELECT version FROM #{sm_table}"
408 ).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
409 end
410
411 def record_version_state_after_migrating(version)
412 super(version.to_s + "-" + current_plugin.id.to_s)
413 end
414 end
280 end
415 end
281 end
416 end
@@ -222,8 +222,8 module Redmine
222 :author => author,
222 :author => author,
223 :message => commit_log.chomp,
223 :message => commit_log.chomp,
224 :paths => [{
224 :paths => [{
225 :revision => revision,
225 :revision => revision.dup,
226 :branch => revBranch,
226 :branch => revBranch.dup,
227 :path => scm_iconv('UTF-8', @path_encoding, entry_path),
227 :path => scm_iconv('UTF-8', @path_encoding, entry_path),
228 :name => scm_iconv('UTF-8', @path_encoding, entry_name),
228 :name => scm_iconv('UTF-8', @path_encoding, entry_name),
229 :kind => 'file',
229 :kind => 'file',
@@ -118,6 +118,14 module ApplicationHelper
118 stylesheet_path source
118 stylesheet_path source
119 end
119 end
120
120
121 def stylesheet_link_tag(source, *args)
122 if current_theme && current_theme.stylesheets.include?(source)
123 super current_theme.stylesheet_path(source), *args
124 else
125 super
126 end
127 end
128
121 # Returns the header tags for the current theme
129 # Returns the header tags for the current theme
122 def heads_for_theme
130 def heads_for_theme
123 if current_theme && current_theme.javascripts.include?('theme')
131 if current_theme && current_theme.javascripts.include?('theme')
@@ -22,7 +22,7 module Redmine
22 def relative_url_root
22 def relative_url_root
23 ActionController::Base.respond_to?('relative_url_root') ?
23 ActionController::Base.respond_to?('relative_url_root') ?
24 ActionController::Base.relative_url_root.to_s :
24 ActionController::Base.relative_url_root.to_s :
25 ActionController::AbstractRequest.relative_url_root.to_s
25 ActionController::Base.config.relative_url_root.to_s
26 end
26 end
27
27
28 # Sets the relative root url of the application
28 # Sets the relative root url of the application
@@ -30,7 +30,7 module Redmine
30 if ActionController::Base.respond_to?('relative_url_root=')
30 if ActionController::Base.respond_to?('relative_url_root=')
31 ActionController::Base.relative_url_root=arg
31 ActionController::Base.relative_url_root=arg
32 else
32 else
33 ActionController::AbstractRequest.relative_url_root=arg
33 ActionController::Base.config.relative_url_root = arg
34 end
34 end
35 end
35 end
36
36
@@ -38,7 +38,7 module Redmine
38 # Example:
38 # Example:
39 # random_hex(4) # => "89b8c729"
39 # random_hex(4) # => "89b8c729"
40 def random_hex(n)
40 def random_hex(n)
41 ActiveSupport::SecureRandom.hex(n)
41 SecureRandom.hex(n)
42 end
42 end
43 end
43 end
44 end
44 end
@@ -2,9 +2,9 require 'rexml/document'
2
2
3 module Redmine
3 module Redmine
4 module VERSION #:nodoc:
4 module VERSION #:nodoc:
5 MAJOR = 1
5 MAJOR = 2
6 MINOR = 4
6 MINOR = 0
7 TINY = 1
7 TINY = 0
8
8
9 # Branch values:
9 # Branch values:
10 # * official release: nil
10 # * official release: nil
@@ -17,10 +17,8
17
17
18 module Redmine
18 module Redmine
19 module Views
19 module Views
20 class ApiTemplateHandler < ActionView::TemplateHandler
20 class ApiTemplateHandler
21 include ActionView::TemplateHandlers::Compilable
21 def self.call(template)
22
23 def compile(template)
24 "Redmine::Views::Builders.for(params[:format]) do |api|; #{template.source}; self.output_buffer = api.output; end"
22 "Redmine::Views::Builders.for(params[:format]) do |api|; #{template.source}; self.output_buffer = api.output; end"
25 end
23 end
26 end
24 end
@@ -15,6 +15,8
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'builder'
19
18 module Redmine
20 module Redmine
19 module Views
21 module Views
20 module Builders
22 module Builders
@@ -29,7 +31,7 module Redmine
29 end
31 end
30
32
31 def method_missing(sym, *args, &block)
33 def method_missing(sym, *args, &block)
32 if args.size == 1 && args.first.is_a?(Time)
34 if args.size == 1 && args.first.is_a?(::Time)
33 __send__ sym, args.first.xmlschema, &block
35 __send__ sym, args.first.xmlschema, &block
34 else
36 else
35 super
37 super
@@ -24,14 +24,14 class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder
24 %w(date_select)).each do |selector|
24 %w(date_select)).each do |selector|
25 src = <<-END_SRC
25 src = <<-END_SRC
26 def #{selector}(field, options = {})
26 def #{selector}(field, options = {})
27 label_for_field(field, options) + super(field, options.except(:label))
27 label_for_field(field, options) + super(field, options.except(:label)).html_safe
28 end
28 end
29 END_SRC
29 END_SRC
30 class_eval src, __FILE__, __LINE__
30 class_eval src, __FILE__, __LINE__
31 end
31 end
32
32
33 def select(field, choices, options = {}, html_options = {})
33 def select(field, choices, options = {}, html_options = {})
34 label_for_field(field, options) + super(field, choices, options, html_options.except(:label))
34 label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe
35 end
35 end
36
36
37 # Returns a label tag for the given field
37 # Returns a label tag for the given field
@@ -20,7 +20,7 module Redmine
20 module MyPage
20 module MyPage
21 module Block
21 module Block
22 def self.additional_blocks
22 def self.additional_blocks
23 @@additional_blocks ||= Dir.glob("#{Rails.root}/vendor/plugins/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
23 @@additional_blocks ||= Dir.glob("#{Redmine::Plugin.directory}/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
24 name = File.basename(file).split('.').first.gsub(/^_/, '')
24 name = File.basename(file).split('.').first.gsub(/^_/, '')
25 h[name] = name.to_sym
25 h[name] = name.to_sym
26 h
26 h
@@ -115,8 +115,9 module Redmine
115 url=url[0..-2] # discard closing parenth from url
115 url=url[0..-2] # discard closing parenth from url
116 post = ")"+post # add closing parenth to post
116 post = ")"+post # add closing parenth to post
117 end
117 end
118 tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
118 content = proto + url
119 %(#{leading}#{tag}#{post})
119 href = "#{proto=="www."?"http://www.":proto}#{url}"
120 %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
120 end
121 end
121 end
122 end
122 end
123 end
@@ -128,7 +129,7 module Redmine
128 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
129 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
129 mail
130 mail
130 else
131 else
131 content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
132 %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
132 end
133 end
133 end
134 end
134 end
135 end
@@ -7,3 +7,4 end
7 deprecated_task :load_default_data, "redmine:load_default_data"
7 deprecated_task :load_default_data, "redmine:load_default_data"
8 deprecated_task :migrate_from_mantis, "redmine:migrate_from_mantis"
8 deprecated_task :migrate_from_mantis, "redmine:migrate_from_mantis"
9 deprecated_task :migrate_from_trac, "redmine:migrate_from_trac"
9 deprecated_task :migrate_from_trac, "redmine:migrate_from_trac"
10 deprecated_task "db:migrate_plugins", "redmine:plugins:migrate"
@@ -1,34 +1,24
1 desc 'Generates a configuration file for cookie store sessions.'
1 desc 'Generates a secret token for the application.'
2
2
3 file 'config/initializers/session_store.rb' do
3 file 'config/initializers/secret_token.rb' do
4 path = File.join(Rails.root, 'config', 'initializers', 'session_store.rb')
4 path = File.join(Rails.root, 'config', 'initializers', 'secret_token.rb')
5 secret = ActiveSupport::SecureRandom.hex(40)
5 secret = SecureRandom.hex(40)
6 File.open(path, 'w') do |f|
6 File.open(path, 'w') do |f|
7 f.write <<"EOF"
7 f.write <<"EOF"
8 # This file was generated by 'rake config/initializers/session_store.rb',
8 # This file was generated by 'rake generate_secret_token', and should
9 # and should not be made visible to public.
9 # not be made visible to public.
10 # If you have a load-balancing Redmine cluster, you will need to use the
10 # If you have a load-balancing Redmine cluster, you will need to use the
11 # same version of this file on each machine. And be sure to restart your
11 # same version of this file on each machine. And be sure to restart your
12 # server when you modify this file.
12 # server when you modify this file.
13
13 #
14 # Your secret key for verifying cookie session data integrity. If you
14 # Your secret key for verifying cookie session data integrity. If you
15 # change this key, all old sessions will become invalid! Make sure the
15 # change this key, all old sessions will become invalid! Make sure the
16 # secret is at least 30 characters and all random, no regular words or
16 # secret is at least 30 characters and all random, no regular words or
17 # you'll be exposed to dictionary attacks.
17 # you'll be exposed to dictionary attacks.
18 ActionController::Base.session = {
18 RedmineApp::Application.config.secret_token = '#{secret}'
19 :key => '_redmine_session',
20 #
21 # Uncomment and edit the :session_path below if are hosting your Redmine
22 # at a suburi and don't want the top level path to access the cookies
23 #
24 # See: http://www.redmine.org/issues/3968
25 #
26 # :session_path => '/url_path_to/your/redmine/',
27 :secret => '#{secret}'
28 }
29 EOF
19 EOF
30 end
20 end
31 end
21 end
32
22
33 desc 'Generates a configuration file for cookie store sessions.'
23 desc 'Generates a secret token for the application.'
34 task :generate_session_store => ['config/initializers/session_store.rb']
24 task :generate_secret_token => ['config/initializers/secret_token.rb']
@@ -41,4 +41,28 namespace :redmine do
41 task :fetch_changesets => :environment do
41 task :fetch_changesets => :environment do
42 Repository.fetch_changesets
42 Repository.fetch_changesets
43 end
43 end
44
45 desc 'Migrates and copies plugins assets.'
46 task :plugins do
47 Rake::Task["redmine:plugins:migrate"].invoke
48 Rake::Task["redmine:plugins:assets"].invoke
49 end
50
51 namespace :plugins do
52 desc 'Migrates installed plugins.'
53 task :migrate => :environment do
54 Redmine::Plugin.all.each do |plugin|
55 puts "Migrating #{plugin.name}..."
56 plugin.migrate
57 end
58 end
59
60 desc 'Copies plugins assets into the public directory.'
61 task :assets => :environment do
62 Redmine::Plugin.all.each do |plugin|
63 puts "Copying #{plugin.name} assets..."
64 plugin.mirror_assets
65 end
66 end
67 end
44 end
68 end
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now