@@ -0,0 +1,54 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
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. | |||
|
17 | ||||
|
18 | require 'loofah/helpers' | |||
|
19 | ||||
|
20 | module Redmine | |||
|
21 | module WikiFormatting | |||
|
22 | class HtmlParser | |||
|
23 | ||||
|
24 | class_attribute :tags | |||
|
25 | self.tags = { | |||
|
26 | 'br' => {:post => "\n"} | |||
|
27 | } | |||
|
28 | ||||
|
29 | def self.to_text(html) | |||
|
30 | html = html.gsub(/[\n\r]/, '').squeeze(' ') | |||
|
31 | ||||
|
32 | doc = Loofah.document(html) | |||
|
33 | doc.scrub!(WikiTags.new(tags)) | |||
|
34 | doc.scrub!(:newline_block_elements) | |||
|
35 | ||||
|
36 | Loofah::Helpers.remove_extraneous_whitespace(doc.text).strip | |||
|
37 | end | |||
|
38 | ||||
|
39 | class WikiTags < ::Loofah::Scrubber | |||
|
40 | def initialize(tags_to_text) | |||
|
41 | @direction = :bottom_up | |||
|
42 | @tags_to_text = tags_to_text || {} | |||
|
43 | end | |||
|
44 | ||||
|
45 | def scrub(node) | |||
|
46 | formatting = @tags_to_text[node.name] | |||
|
47 | return CONTINUE unless formatting | |||
|
48 | node.add_next_sibling Nokogiri::XML::Text.new("#{formatting[:pre]}#{node.content}#{formatting[:post]}", node.document) | |||
|
49 | node.remove | |||
|
50 | end | |||
|
51 | end | |||
|
52 | end | |||
|
53 | end | |||
|
54 | end |
@@ -0,0 +1,40 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
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. | |||
|
17 | ||||
|
18 | module Redmine | |||
|
19 | module WikiFormatting | |||
|
20 | module Markdown | |||
|
21 | class HtmlParser < Redmine::WikiFormatting::HtmlParser | |||
|
22 | ||||
|
23 | self.tags = { | |||
|
24 | 'b' => {:pre => '**', :post => '**'}, | |||
|
25 | 'strong' => {:pre => '**', :post => '**'}, | |||
|
26 | 'i' => {:pre => '_', :post => '_'}, | |||
|
27 | 'em' => {:pre => '_', :post => '_'}, | |||
|
28 | 'strike' => {:pre => '~~', :post => '~~'}, | |||
|
29 | 'br' => {:post => "\n"}, | |||
|
30 | 'h1' => {:pre => "\n\n# ", :post => "\n\n"}, | |||
|
31 | 'h2' => {:pre => "\n\n## ", :post => "\n\n"}, | |||
|
32 | 'h3' => {:pre => "\n\n### ", :post => "\n\n"}, | |||
|
33 | 'h4' => {:pre => "\n\n#### ", :post => "\n\n"}, | |||
|
34 | 'h5' => {:pre => "\n\n##### ", :post => "\n\n"}, | |||
|
35 | 'h6' => {:pre => "\n\n###### ", :post => "\n\n"} | |||
|
36 | } | |||
|
37 | end | |||
|
38 | end | |||
|
39 | end | |||
|
40 | end |
@@ -0,0 +1,41 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
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. | |||
|
17 | ||||
|
18 | module Redmine | |||
|
19 | module WikiFormatting | |||
|
20 | module Textile | |||
|
21 | class HtmlParser < Redmine::WikiFormatting::HtmlParser | |||
|
22 | ||||
|
23 | self.tags = { | |||
|
24 | 'b' => {:pre => '*', :post => '*'}, | |||
|
25 | 'strong' => {:pre => '*', :post => '*'}, | |||
|
26 | 'i' => {:pre => '_', :post => '_'}, | |||
|
27 | 'em' => {:pre => '_', :post => '_'}, | |||
|
28 | 'u' => {:pre => '+', :post => '+'}, | |||
|
29 | 'strike' => {:pre => '-', :post => '-'}, | |||
|
30 | 'br' => {:post => "\n"}, | |||
|
31 | 'h1' => {:pre => "\n\nh1. ", :post => "\n\n"}, | |||
|
32 | 'h2' => {:pre => "\n\nh2. ", :post => "\n\n"}, | |||
|
33 | 'h3' => {:pre => "\n\nh3. ", :post => "\n\n"}, | |||
|
34 | 'h4' => {:pre => "\n\nh4. ", :post => "\n\n"}, | |||
|
35 | 'h5' => {:pre => "\n\nh5. ", :post => "\n\n"}, | |||
|
36 | 'h6' => {:pre => "\n\nh6. ", :post => "\n\n"} | |||
|
37 | } | |||
|
38 | end | |||
|
39 | end | |||
|
40 | end | |||
|
41 | end |
This diff has been collapsed as it changes many lines, (966 lines changed) Show them Hide them | |||||
@@ -0,0 +1,966 | |||||
|
1 | From: jsmith@somenet.foo | |||
|
2 | To: testuser@example.org | |||
|
3 | Subject: =?utf-8?Q?Test_email?= | |||
|
4 | Date: Mon, 11 May 2015 10:50:31 -0500 | |||
|
5 | MIME-Version: 1.0 | |||
|
6 | Content-Type: multipart/alternative; | |||
|
7 | boundary="Mark=_539924359269962179476" | |||
|
8 | X-Priority: 3 | |||
|
9 | ||||
|
10 | This is a multi-part message in MIME format. | |||
|
11 | ||||
|
12 | --Mark=_539924359269962179476 | |||
|
13 | Content-Type: text/plain; | |||
|
14 | charset="utf-8" | |||
|
15 | Content-Transfer-Encoding: quoted-printable | |||
|
16 | ||||
|
17 | Simple, unadorned test email generated by Outlook 2010. It is in HTML f= | |||
|
18 | ormat, but no special formatting has been chosen. I=E2=80=99m going to = | |||
|
19 | save this as a draft and then manually drop it into the Inbox for scrap= | |||
|
20 | ing by Redmine 3.0.2. | |||
|
21 | ||||
|
22 | --Mark=_539924359269962179476 | |||
|
23 | Content-Type: text/html; | |||
|
24 | charset="utf-8" | |||
|
25 | Content-Transfer-Encoding: quoted-printable | |||
|
26 | ||||
|
27 | <STYLE> | |||
|
28 | pre { | |||
|
29 | white-space: pre-wrap; /* css-3 */ | |||
|
30 | white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ | |||
|
31 | white-space: -pre-wrap; /* Opera 4-6 */ | |||
|
32 | white-space: -o-pre-wrap; /* Opera 7 */ | |||
|
33 | word-wrap: break-word; /* Internet Explorer 5.5+ */ | |||
|
34 | } | |||
|
35 | </STYLE> | |||
|
36 | <html xmlns:v=3D"urn:schemas-microsoft-com:vml" xmlns:o=3D"urn:schemas-= | |||
|
37 | microsoft-com:office:office" xmlns:w=3D"urn:schemas-microsoft-com:offic= | |||
|
38 | e:word" xmlns:m=3D"http://schemas.microsoft.com/office/2004/12/omml" xm= | |||
|
39 | lns=3D"http://www.w3.org/TR/REC-html40"><head><meta name=3DProgId conte= | |||
|
40 | nt=3DWord.Document><meta name=3DGenerator content=3D"Microsoft Word 15"= | |||
|
41 | ><meta name=3DOriginator content=3D"Microsoft Word 15"><link rel=3DFile= | |||
|
42 | -List href=3D"cid:filelist.xml@01D08BD8.4D051580"><!--[if gte mso 9]><x= | |||
|
43 | ml> | |||
|
44 | <o:OfficeDocumentSettings> | |||
|
45 | <o:AllowPNG/> | |||
|
46 | </o:OfficeDocumentSettings> | |||
|
47 | </xml><![endif]--><link rel=3DthemeData href=3D"~~themedata~~"><link re= | |||
|
48 | l=3DcolorSchemeMapping href=3D"~~colorschememapping~~"><!--[if gte mso = | |||
|
49 | 9]><xml> | |||
|
50 | <w:WordDocument> | |||
|
51 | <w:Zoom>120</w:Zoom> | |||
|
52 | <w:SpellingState>Clean</w:SpellingState> | |||
|
53 | <w:TrackMoves/> | |||
|
54 | <w:TrackFormatting/> | |||
|
55 | <w:EnvelopeVis/> | |||
|
56 | <w:PunctuationKerning/> | |||
|
57 | <w:ValidateAgainstSchemas/> | |||
|
58 | <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> | |||
|
59 | <w:IgnoreMixedContent>false</w:IgnoreMixedContent> | |||
|
60 | <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> | |||
|
61 | <w:DoNotPromoteQF/> | |||
|
62 | <w:LidThemeOther>EN-US</w:LidThemeOther> | |||
|
63 | <w:LidThemeAsian>X-NONE</w:LidThemeAsian> | |||
|
64 | <w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript> | |||
|
65 | <w:Compatibility> | |||
|
66 | <w:DoNotExpandShiftReturn/> | |||
|
67 | <w:BreakWrappedTables/> | |||
|
68 | <w:SnapToGridInCell/> | |||
|
69 | <w:WrapTextWithPunct/> | |||
|
70 | <w:UseAsianBreakRules/> | |||
|
71 | <w:DontGrowAutofit/> | |||
|
72 | <w:SplitPgBreakAndParaMark/> | |||
|
73 | <w:EnableOpenTypeKerning/> | |||
|
74 | <w:DontFlipMirrorIndents/> | |||
|
75 | <w:OverrideTableStyleHps/> | |||
|
76 | </w:Compatibility> | |||
|
77 | <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> | |||
|
78 | <m:mathPr> | |||
|
79 | <m:mathFont m:val=3D"Cambria Math"/> | |||
|
80 | <m:brkBin m:val=3D"before"/> | |||
|
81 | <m:brkBinSub m:val=3D"--"/> | |||
|
82 | <m:smallFrac m:val=3D"off"/> | |||
|
83 | <m:dispDef/> | |||
|
84 | <m:lMargin m:val=3D"0"/> | |||
|
85 | <m:rMargin m:val=3D"0"/> | |||
|
86 | <m:defJc m:val=3D"centerGroup"/> | |||
|
87 | <m:wrapIndent m:val=3D"1440"/> | |||
|
88 | <m:intLim m:val=3D"subSup"/> | |||
|
89 | <m:naryLim m:val=3D"undOvr"/> | |||
|
90 | </m:mathPr></w:WordDocument> | |||
|
91 | </xml><![endif]--><!--[if gte mso 9]><xml> | |||
|
92 | <w:LatentStyles DefLockedState=3D"false" DefUnhideWhenUsed=3D"false" De= | |||
|
93 | fSemiHidden=3D"false" DefQFormat=3D"false" DefPriority=3D"99" LatentSty= | |||
|
94 | leCount=3D"371"> | |||
|
95 | <w:LsdException Locked=3D"false" Priority=3D"0" QFormat=3D"true" Name=3D= | |||
|
96 | "Normal"/> | |||
|
97 | <w:LsdException Locked=3D"false" Priority=3D"9" QFormat=3D"true" Name=3D= | |||
|
98 | "heading 1"/> | |||
|
99 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
100 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 2"/> | |||
|
101 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
102 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 3"/> | |||
|
103 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
104 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 4"/> | |||
|
105 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
106 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 5"/> | |||
|
107 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
108 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 6"/> | |||
|
109 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
110 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 7"/> | |||
|
111 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
112 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 8"/> | |||
|
113 | <w:LsdException Locked=3D"false" Priority=3D"9" SemiHidden=3D"true" Unh= | |||
|
114 | ideWhenUsed=3D"true" QFormat=3D"true" Name=3D"heading 9"/> | |||
|
115 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
116 | true" Name=3D"index 1"/> | |||
|
117 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
118 | true" Name=3D"index 2"/> | |||
|
119 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
120 | true" Name=3D"index 3"/> | |||
|
121 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
122 | true" Name=3D"index 4"/> | |||
|
123 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
124 | true" Name=3D"index 5"/> | |||
|
125 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
126 | true" Name=3D"index 6"/> | |||
|
127 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
128 | true" Name=3D"index 7"/> | |||
|
129 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
130 | true" Name=3D"index 8"/> | |||
|
131 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
132 | true" Name=3D"index 9"/> | |||
|
133 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
134 | hideWhenUsed=3D"true" Name=3D"toc 1"/> | |||
|
135 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
136 | hideWhenUsed=3D"true" Name=3D"toc 2"/> | |||
|
137 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
138 | hideWhenUsed=3D"true" Name=3D"toc 3"/> | |||
|
139 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
140 | hideWhenUsed=3D"true" Name=3D"toc 4"/> | |||
|
141 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
142 | hideWhenUsed=3D"true" Name=3D"toc 5"/> | |||
|
143 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
144 | hideWhenUsed=3D"true" Name=3D"toc 6"/> | |||
|
145 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
146 | hideWhenUsed=3D"true" Name=3D"toc 7"/> | |||
|
147 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
148 | hideWhenUsed=3D"true" Name=3D"toc 8"/> | |||
|
149 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
150 | hideWhenUsed=3D"true" Name=3D"toc 9"/> | |||
|
151 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
152 | true" Name=3D"Normal Indent"/> | |||
|
153 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
154 | true" Name=3D"footnote text"/> | |||
|
155 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
156 | true" Name=3D"annotation text"/> | |||
|
157 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
158 | true" Name=3D"header"/> | |||
|
159 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
160 | true" Name=3D"footer"/> | |||
|
161 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
162 | true" Name=3D"index heading"/> | |||
|
163 | <w:LsdException Locked=3D"false" Priority=3D"35" SemiHidden=3D"true" Un= | |||
|
164 | hideWhenUsed=3D"true" QFormat=3D"true" Name=3D"caption"/> | |||
|
165 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
166 | true" Name=3D"table of figures"/> | |||
|
167 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
168 | true" Name=3D"envelope address"/> | |||
|
169 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
170 | true" Name=3D"envelope return"/> | |||
|
171 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
172 | true" Name=3D"footnote reference"/> | |||
|
173 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
174 | true" Name=3D"annotation reference"/> | |||
|
175 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
176 | true" Name=3D"line number"/> | |||
|
177 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
178 | true" Name=3D"page number"/> | |||
|
179 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
180 | true" Name=3D"endnote reference"/> | |||
|
181 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
182 | true" Name=3D"endnote text"/> | |||
|
183 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
184 | true" Name=3D"table of authorities"/> | |||
|
185 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
186 | true" Name=3D"macro"/> | |||
|
187 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
188 | true" Name=3D"toa heading"/> | |||
|
189 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
190 | true" Name=3D"List"/> | |||
|
191 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
192 | true" Name=3D"List Bullet"/> | |||
|
193 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
194 | true" Name=3D"List Number"/> | |||
|
195 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
196 | true" Name=3D"List 2"/> | |||
|
197 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
198 | true" Name=3D"List 3"/> | |||
|
199 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
200 | true" Name=3D"List 4"/> | |||
|
201 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
202 | true" Name=3D"List 5"/> | |||
|
203 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
204 | true" Name=3D"List Bullet 2"/> | |||
|
205 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
206 | true" Name=3D"List Bullet 3"/> | |||
|
207 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
208 | true" Name=3D"List Bullet 4"/> | |||
|
209 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
210 | true" Name=3D"List Bullet 5"/> | |||
|
211 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
212 | true" Name=3D"List Number 2"/> | |||
|
213 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
214 | true" Name=3D"List Number 3"/> | |||
|
215 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
216 | true" Name=3D"List Number 4"/> | |||
|
217 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
218 | true" Name=3D"List Number 5"/> | |||
|
219 | <w:LsdException Locked=3D"false" Priority=3D"10" QFormat=3D"true" Name=3D= | |||
|
220 | "Title"/> | |||
|
221 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
222 | true" Name=3D"Closing"/> | |||
|
223 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
224 | true" Name=3D"Signature"/> | |||
|
225 | <w:LsdException Locked=3D"false" Priority=3D"1" SemiHidden=3D"true" Unh= | |||
|
226 | ideWhenUsed=3D"true" Name=3D"Default Paragraph Font"/> | |||
|
227 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
228 | true" Name=3D"Body Text"/> | |||
|
229 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
230 | true" Name=3D"Body Text Indent"/> | |||
|
231 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
232 | true" Name=3D"List Continue"/> | |||
|
233 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
234 | true" Name=3D"List Continue 2"/> | |||
|
235 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
236 | true" Name=3D"List Continue 3"/> | |||
|
237 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
238 | true" Name=3D"List Continue 4"/> | |||
|
239 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
240 | true" Name=3D"List Continue 5"/> | |||
|
241 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
242 | true" Name=3D"Message Header"/> | |||
|
243 | <w:LsdException Locked=3D"false" Priority=3D"11" QFormat=3D"true" Name=3D= | |||
|
244 | "Subtitle"/> | |||
|
245 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
246 | true" Name=3D"Salutation"/> | |||
|
247 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
248 | true" Name=3D"Date"/> | |||
|
249 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
250 | true" Name=3D"Body Text First Indent"/> | |||
|
251 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
252 | true" Name=3D"Body Text First Indent 2"/> | |||
|
253 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
254 | true" Name=3D"Note Heading"/> | |||
|
255 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
256 | true" Name=3D"Body Text 2"/> | |||
|
257 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
258 | true" Name=3D"Body Text 3"/> | |||
|
259 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
260 | true" Name=3D"Body Text Indent 2"/> | |||
|
261 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
262 | true" Name=3D"Body Text Indent 3"/> | |||
|
263 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
264 | true" Name=3D"Block Text"/> | |||
|
265 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
266 | true" Name=3D"Hyperlink"/> | |||
|
267 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
268 | true" Name=3D"FollowedHyperlink"/> | |||
|
269 | <w:LsdException Locked=3D"false" Priority=3D"22" QFormat=3D"true" Name=3D= | |||
|
270 | "Strong"/> | |||
|
271 | <w:LsdException Locked=3D"false" Priority=3D"20" QFormat=3D"true" Name=3D= | |||
|
272 | "Emphasis"/> | |||
|
273 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
274 | true" Name=3D"Document Map"/> | |||
|
275 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
276 | true" Name=3D"Plain Text"/> | |||
|
277 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
278 | true" Name=3D"E-mail Signature"/> | |||
|
279 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
280 | true" Name=3D"HTML Top of Form"/> | |||
|
281 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
282 | true" Name=3D"HTML Bottom of Form"/> | |||
|
283 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
284 | true" Name=3D"Normal (Web)"/> | |||
|
285 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
286 | true" Name=3D"HTML Acronym"/> | |||
|
287 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
288 | true" Name=3D"HTML Address"/> | |||
|
289 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
290 | true" Name=3D"HTML Cite"/> | |||
|
291 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
292 | true" Name=3D"HTML Code"/> | |||
|
293 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
294 | true" Name=3D"HTML Definition"/> | |||
|
295 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
296 | true" Name=3D"HTML Keyboard"/> | |||
|
297 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
298 | true" Name=3D"HTML Preformatted"/> | |||
|
299 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
300 | true" Name=3D"HTML Sample"/> | |||
|
301 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
302 | true" Name=3D"HTML Typewriter"/> | |||
|
303 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
304 | true" Name=3D"HTML Variable"/> | |||
|
305 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
306 | true" Name=3D"Normal Table"/> | |||
|
307 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
308 | true" Name=3D"annotation subject"/> | |||
|
309 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
310 | true" Name=3D"No List"/> | |||
|
311 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
312 | true" Name=3D"Outline List 1"/> | |||
|
313 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
314 | true" Name=3D"Outline List 2"/> | |||
|
315 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
316 | true" Name=3D"Outline List 3"/> | |||
|
317 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
318 | true" Name=3D"Table Simple 1"/> | |||
|
319 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
320 | true" Name=3D"Table Simple 2"/> | |||
|
321 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
322 | true" Name=3D"Table Simple 3"/> | |||
|
323 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
324 | true" Name=3D"Table Classic 1"/> | |||
|
325 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
326 | true" Name=3D"Table Classic 2"/> | |||
|
327 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
328 | true" Name=3D"Table Classic 3"/> | |||
|
329 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
330 | true" Name=3D"Table Classic 4"/> | |||
|
331 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
332 | true" Name=3D"Table Colorful 1"/> | |||
|
333 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
334 | true" Name=3D"Table Colorful 2"/> | |||
|
335 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
336 | true" Name=3D"Table Colorful 3"/> | |||
|
337 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
338 | true" Name=3D"Table Columns 1"/> | |||
|
339 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
340 | true" Name=3D"Table Columns 2"/> | |||
|
341 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
342 | true" Name=3D"Table Columns 3"/> | |||
|
343 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
344 | true" Name=3D"Table Columns 4"/> | |||
|
345 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
346 | true" Name=3D"Table Columns 5"/> | |||
|
347 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
348 | true" Name=3D"Table Grid 1"/> | |||
|
349 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
350 | true" Name=3D"Table Grid 2"/> | |||
|
351 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
352 | true" Name=3D"Table Grid 3"/> | |||
|
353 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
354 | true" Name=3D"Table Grid 4"/> | |||
|
355 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
356 | true" Name=3D"Table Grid 5"/> | |||
|
357 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
358 | true" Name=3D"Table Grid 6"/> | |||
|
359 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
360 | true" Name=3D"Table Grid 7"/> | |||
|
361 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
362 | true" Name=3D"Table Grid 8"/> | |||
|
363 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
364 | true" Name=3D"Table List 1"/> | |||
|
365 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
366 | true" Name=3D"Table List 2"/> | |||
|
367 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
368 | true" Name=3D"Table List 3"/> | |||
|
369 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
370 | true" Name=3D"Table List 4"/> | |||
|
371 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
372 | true" Name=3D"Table List 5"/> | |||
|
373 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
374 | true" Name=3D"Table List 6"/> | |||
|
375 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
376 | true" Name=3D"Table List 7"/> | |||
|
377 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
378 | true" Name=3D"Table List 8"/> | |||
|
379 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
380 | true" Name=3D"Table 3D effects 1"/> | |||
|
381 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
382 | true" Name=3D"Table 3D effects 2"/> | |||
|
383 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
384 | true" Name=3D"Table 3D effects 3"/> | |||
|
385 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
386 | true" Name=3D"Table Contemporary"/> | |||
|
387 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
388 | true" Name=3D"Table Elegant"/> | |||
|
389 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
390 | true" Name=3D"Table Professional"/> | |||
|
391 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
392 | true" Name=3D"Table Subtle 1"/> | |||
|
393 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
394 | true" Name=3D"Table Subtle 2"/> | |||
|
395 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
396 | true" Name=3D"Table Web 1"/> | |||
|
397 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
398 | true" Name=3D"Table Web 2"/> | |||
|
399 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
400 | true" Name=3D"Table Web 3"/> | |||
|
401 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
402 | true" Name=3D"Balloon Text"/> | |||
|
403 | <w:LsdException Locked=3D"false" Priority=3D"59" Name=3D"Table Grid"/> | |||
|
404 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" UnhideWhenUsed=3D"= | |||
|
405 | true" Name=3D"Table Theme"/> | |||
|
406 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" Name=3D"Placeholde= | |||
|
407 | r Text"/> | |||
|
408 | <w:LsdException Locked=3D"false" Priority=3D"1" QFormat=3D"true" Name=3D= | |||
|
409 | "No Spacing"/> | |||
|
410 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading"= | |||
|
411 | /> | |||
|
412 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List"/> | |||
|
413 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid"/> | |||
|
414 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
415 | 1"/> | |||
|
416 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
417 | 2"/> | |||
|
418 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1"= | |||
|
419 | /> | |||
|
420 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2"= | |||
|
421 | /> | |||
|
422 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1"= | |||
|
423 | /> | |||
|
424 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2"= | |||
|
425 | /> | |||
|
426 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3"= | |||
|
427 | /> | |||
|
428 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List"/> | |||
|
429 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
430 | ng"/> | |||
|
431 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List"= | |||
|
432 | /> | |||
|
433 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid"= | |||
|
434 | /> | |||
|
435 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading = | |||
|
436 | Accent 1"/> | |||
|
437 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List Acc= | |||
|
438 | ent 1"/> | |||
|
439 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid Acc= | |||
|
440 | ent 1"/> | |||
|
441 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
442 | 1 Accent 1"/> | |||
|
443 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
444 | 2 Accent 1"/> | |||
|
445 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1 = | |||
|
446 | Accent 1"/> | |||
|
447 | <w:LsdException Locked=3D"false" SemiHidden=3D"true" Name=3D"Revision"/= | |||
|
448 | > | |||
|
449 | <w:LsdException Locked=3D"false" Priority=3D"34" QFormat=3D"true" Name=3D= | |||
|
450 | "List Paragraph"/> | |||
|
451 | <w:LsdException Locked=3D"false" Priority=3D"29" QFormat=3D"true" Name=3D= | |||
|
452 | "Quote"/> | |||
|
453 | <w:LsdException Locked=3D"false" Priority=3D"30" QFormat=3D"true" Name=3D= | |||
|
454 | "Intense Quote"/> | |||
|
455 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2 = | |||
|
456 | Accent 1"/> | |||
|
457 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1 = | |||
|
458 | Accent 1"/> | |||
|
459 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2 = | |||
|
460 | Accent 1"/> | |||
|
461 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3 = | |||
|
462 | Accent 1"/> | |||
|
463 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List Acce= | |||
|
464 | nt 1"/> | |||
|
465 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
466 | ng Accent 1"/> | |||
|
467 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List = | |||
|
468 | Accent 1"/> | |||
|
469 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid = | |||
|
470 | Accent 1"/> | |||
|
471 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading = | |||
|
472 | Accent 2"/> | |||
|
473 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List Acc= | |||
|
474 | ent 2"/> | |||
|
475 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid Acc= | |||
|
476 | ent 2"/> | |||
|
477 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
478 | 1 Accent 2"/> | |||
|
479 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
480 | 2 Accent 2"/> | |||
|
481 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1 = | |||
|
482 | Accent 2"/> | |||
|
483 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2 = | |||
|
484 | Accent 2"/> | |||
|
485 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1 = | |||
|
486 | Accent 2"/> | |||
|
487 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2 = | |||
|
488 | Accent 2"/> | |||
|
489 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3 = | |||
|
490 | Accent 2"/> | |||
|
491 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List Acce= | |||
|
492 | nt 2"/> | |||
|
493 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
494 | ng Accent 2"/> | |||
|
495 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List = | |||
|
496 | Accent 2"/> | |||
|
497 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid = | |||
|
498 | Accent 2"/> | |||
|
499 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading = | |||
|
500 | Accent 3"/> | |||
|
501 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List Acc= | |||
|
502 | ent 3"/> | |||
|
503 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid Acc= | |||
|
504 | ent 3"/> | |||
|
505 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
506 | 1 Accent 3"/> | |||
|
507 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
508 | 2 Accent 3"/> | |||
|
509 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1 = | |||
|
510 | Accent 3"/> | |||
|
511 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2 = | |||
|
512 | Accent 3"/> | |||
|
513 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1 = | |||
|
514 | Accent 3"/> | |||
|
515 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2 = | |||
|
516 | Accent 3"/> | |||
|
517 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3 = | |||
|
518 | Accent 3"/> | |||
|
519 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List Acce= | |||
|
520 | nt 3"/> | |||
|
521 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
522 | ng Accent 3"/> | |||
|
523 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List = | |||
|
524 | Accent 3"/> | |||
|
525 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid = | |||
|
526 | Accent 3"/> | |||
|
527 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading = | |||
|
528 | Accent 4"/> | |||
|
529 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List Acc= | |||
|
530 | ent 4"/> | |||
|
531 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid Acc= | |||
|
532 | ent 4"/> | |||
|
533 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
534 | 1 Accent 4"/> | |||
|
535 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
536 | 2 Accent 4"/> | |||
|
537 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1 = | |||
|
538 | Accent 4"/> | |||
|
539 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2 = | |||
|
540 | Accent 4"/> | |||
|
541 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1 = | |||
|
542 | Accent 4"/> | |||
|
543 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2 = | |||
|
544 | Accent 4"/> | |||
|
545 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3 = | |||
|
546 | Accent 4"/> | |||
|
547 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List Acce= | |||
|
548 | nt 4"/> | |||
|
549 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
550 | ng Accent 4"/> | |||
|
551 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List = | |||
|
552 | Accent 4"/> | |||
|
553 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid = | |||
|
554 | Accent 4"/> | |||
|
555 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading = | |||
|
556 | Accent 5"/> | |||
|
557 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List Acc= | |||
|
558 | ent 5"/> | |||
|
559 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid Acc= | |||
|
560 | ent 5"/> | |||
|
561 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
562 | 1 Accent 5"/> | |||
|
563 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
564 | 2 Accent 5"/> | |||
|
565 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1 = | |||
|
566 | Accent 5"/> | |||
|
567 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2 = | |||
|
568 | Accent 5"/> | |||
|
569 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1 = | |||
|
570 | Accent 5"/> | |||
|
571 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2 = | |||
|
572 | Accent 5"/> | |||
|
573 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3 = | |||
|
574 | Accent 5"/> | |||
|
575 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List Acce= | |||
|
576 | nt 5"/> | |||
|
577 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
578 | ng Accent 5"/> | |||
|
579 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List = | |||
|
580 | Accent 5"/> | |||
|
581 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid = | |||
|
582 | Accent 5"/> | |||
|
583 | <w:LsdException Locked=3D"false" Priority=3D"60" Name=3D"Light Shading = | |||
|
584 | Accent 6"/> | |||
|
585 | <w:LsdException Locked=3D"false" Priority=3D"61" Name=3D"Light List Acc= | |||
|
586 | ent 6"/> | |||
|
587 | <w:LsdException Locked=3D"false" Priority=3D"62" Name=3D"Light Grid Acc= | |||
|
588 | ent 6"/> | |||
|
589 | <w:LsdException Locked=3D"false" Priority=3D"63" Name=3D"Medium Shading= | |||
|
590 | 1 Accent 6"/> | |||
|
591 | <w:LsdException Locked=3D"false" Priority=3D"64" Name=3D"Medium Shading= | |||
|
592 | 2 Accent 6"/> | |||
|
593 | <w:LsdException Locked=3D"false" Priority=3D"65" Name=3D"Medium List 1 = | |||
|
594 | Accent 6"/> | |||
|
595 | <w:LsdException Locked=3D"false" Priority=3D"66" Name=3D"Medium List 2 = | |||
|
596 | Accent 6"/> | |||
|
597 | <w:LsdException Locked=3D"false" Priority=3D"67" Name=3D"Medium Grid 1 = | |||
|
598 | Accent 6"/> | |||
|
599 | <w:LsdException Locked=3D"false" Priority=3D"68" Name=3D"Medium Grid 2 = | |||
|
600 | Accent 6"/> | |||
|
601 | <w:LsdException Locked=3D"false" Priority=3D"69" Name=3D"Medium Grid 3 = | |||
|
602 | Accent 6"/> | |||
|
603 | <w:LsdException Locked=3D"false" Priority=3D"70" Name=3D"Dark List Acce= | |||
|
604 | nt 6"/> | |||
|
605 | <w:LsdException Locked=3D"false" Priority=3D"71" Name=3D"Colorful Shadi= | |||
|
606 | ng Accent 6"/> | |||
|
607 | <w:LsdException Locked=3D"false" Priority=3D"72" Name=3D"Colorful List = | |||
|
608 | Accent 6"/> | |||
|
609 | <w:LsdException Locked=3D"false" Priority=3D"73" Name=3D"Colorful Grid = | |||
|
610 | Accent 6"/> | |||
|
611 | <w:LsdException Locked=3D"false" Priority=3D"19" QFormat=3D"true" Name=3D= | |||
|
612 | "Subtle Emphasis"/> | |||
|
613 | <w:LsdException Locked=3D"false" Priority=3D"21" QFormat=3D"true" Name=3D= | |||
|
614 | "Intense Emphasis"/> | |||
|
615 | <w:LsdException Locked=3D"false" Priority=3D"31" QFormat=3D"true" Name=3D= | |||
|
616 | "Subtle Reference"/> | |||
|
617 | <w:LsdException Locked=3D"false" Priority=3D"32" QFormat=3D"true" Name=3D= | |||
|
618 | "Intense Reference"/> | |||
|
619 | <w:LsdException Locked=3D"false" Priority=3D"33" QFormat=3D"true" Name=3D= | |||
|
620 | "Book Title"/> | |||
|
621 | <w:LsdException Locked=3D"false" Priority=3D"37" SemiHidden=3D"true" Un= | |||
|
622 | hideWhenUsed=3D"true" Name=3D"Bibliography"/> | |||
|
623 | <w:LsdException Locked=3D"false" Priority=3D"39" SemiHidden=3D"true" Un= | |||
|
624 | hideWhenUsed=3D"true" QFormat=3D"true" Name=3D"TOC Heading"/> | |||
|
625 | <w:LsdException Locked=3D"false" Priority=3D"41" Name=3D"Plain Table 1"= | |||
|
626 | /> | |||
|
627 | <w:LsdException Locked=3D"false" Priority=3D"42" Name=3D"Plain Table 2"= | |||
|
628 | /> | |||
|
629 | <w:LsdException Locked=3D"false" Priority=3D"43" Name=3D"Plain Table 3"= | |||
|
630 | /> | |||
|
631 | <w:LsdException Locked=3D"false" Priority=3D"44" Name=3D"Plain Table 4"= | |||
|
632 | /> | |||
|
633 | <w:LsdException Locked=3D"false" Priority=3D"45" Name=3D"Plain Table 5"= | |||
|
634 | /> | |||
|
635 | <w:LsdException Locked=3D"false" Priority=3D"40" Name=3D"Grid Table Lig= | |||
|
636 | ht"/> | |||
|
637 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
638 | ight"/> | |||
|
639 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2"/= | |||
|
640 | > | |||
|
641 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3"/= | |||
|
642 | > | |||
|
643 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4"/= | |||
|
644 | > | |||
|
645 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
646 | ark"/> | |||
|
647 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
648 | olorful"/> | |||
|
649 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
650 | olorful"/> | |||
|
651 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
652 | ight Accent 1"/> | |||
|
653 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2 A= | |||
|
654 | ccent 1"/> | |||
|
655 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3 A= | |||
|
656 | ccent 1"/> | |||
|
657 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4 A= | |||
|
658 | ccent 1"/> | |||
|
659 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
660 | ark Accent 1"/> | |||
|
661 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
662 | olorful Accent 1"/> | |||
|
663 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
664 | olorful Accent 1"/> | |||
|
665 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
666 | ight Accent 2"/> | |||
|
667 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2 A= | |||
|
668 | ccent 2"/> | |||
|
669 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3 A= | |||
|
670 | ccent 2"/> | |||
|
671 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4 A= | |||
|
672 | ccent 2"/> | |||
|
673 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
674 | ark Accent 2"/> | |||
|
675 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
676 | olorful Accent 2"/> | |||
|
677 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
678 | olorful Accent 2"/> | |||
|
679 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
680 | ight Accent 3"/> | |||
|
681 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2 A= | |||
|
682 | ccent 3"/> | |||
|
683 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3 A= | |||
|
684 | ccent 3"/> | |||
|
685 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4 A= | |||
|
686 | ccent 3"/> | |||
|
687 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
688 | ark Accent 3"/> | |||
|
689 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
690 | olorful Accent 3"/> | |||
|
691 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
692 | olorful Accent 3"/> | |||
|
693 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
694 | ight Accent 4"/> | |||
|
695 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2 A= | |||
|
696 | ccent 4"/> | |||
|
697 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3 A= | |||
|
698 | ccent 4"/> | |||
|
699 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4 A= | |||
|
700 | ccent 4"/> | |||
|
701 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
702 | ark Accent 4"/> | |||
|
703 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
704 | olorful Accent 4"/> | |||
|
705 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
706 | olorful Accent 4"/> | |||
|
707 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
708 | ight Accent 5"/> | |||
|
709 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2 A= | |||
|
710 | ccent 5"/> | |||
|
711 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3 A= | |||
|
712 | ccent 5"/> | |||
|
713 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4 A= | |||
|
714 | ccent 5"/> | |||
|
715 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
716 | ark Accent 5"/> | |||
|
717 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
718 | olorful Accent 5"/> | |||
|
719 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
720 | olorful Accent 5"/> | |||
|
721 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"Grid Table 1 L= | |||
|
722 | ight Accent 6"/> | |||
|
723 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"Grid Table 2 A= | |||
|
724 | ccent 6"/> | |||
|
725 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"Grid Table 3 A= | |||
|
726 | ccent 6"/> | |||
|
727 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"Grid Table 4 A= | |||
|
728 | ccent 6"/> | |||
|
729 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"Grid Table 5 D= | |||
|
730 | ark Accent 6"/> | |||
|
731 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"Grid Table 6 C= | |||
|
732 | olorful Accent 6"/> | |||
|
733 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"Grid Table 7 C= | |||
|
734 | olorful Accent 6"/> | |||
|
735 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
736 | ight"/> | |||
|
737 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2"/= | |||
|
738 | > | |||
|
739 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3"/= | |||
|
740 | > | |||
|
741 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4"/= | |||
|
742 | > | |||
|
743 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
744 | ark"/> | |||
|
745 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
746 | olorful"/> | |||
|
747 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
748 | olorful"/> | |||
|
749 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
750 | ight Accent 1"/> | |||
|
751 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2 A= | |||
|
752 | ccent 1"/> | |||
|
753 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3 A= | |||
|
754 | ccent 1"/> | |||
|
755 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4 A= | |||
|
756 | ccent 1"/> | |||
|
757 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
758 | ark Accent 1"/> | |||
|
759 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
760 | olorful Accent 1"/> | |||
|
761 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
762 | olorful Accent 1"/> | |||
|
763 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
764 | ight Accent 2"/> | |||
|
765 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2 A= | |||
|
766 | ccent 2"/> | |||
|
767 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3 A= | |||
|
768 | ccent 2"/> | |||
|
769 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4 A= | |||
|
770 | ccent 2"/> | |||
|
771 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
772 | ark Accent 2"/> | |||
|
773 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
774 | olorful Accent 2"/> | |||
|
775 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
776 | olorful Accent 2"/> | |||
|
777 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
778 | ight Accent 3"/> | |||
|
779 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2 A= | |||
|
780 | ccent 3"/> | |||
|
781 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3 A= | |||
|
782 | ccent 3"/> | |||
|
783 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4 A= | |||
|
784 | ccent 3"/> | |||
|
785 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
786 | ark Accent 3"/> | |||
|
787 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
788 | olorful Accent 3"/> | |||
|
789 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
790 | olorful Accent 3"/> | |||
|
791 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
792 | ight Accent 4"/> | |||
|
793 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2 A= | |||
|
794 | ccent 4"/> | |||
|
795 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3 A= | |||
|
796 | ccent 4"/> | |||
|
797 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4 A= | |||
|
798 | ccent 4"/> | |||
|
799 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
800 | ark Accent 4"/> | |||
|
801 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
802 | olorful Accent 4"/> | |||
|
803 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
804 | olorful Accent 4"/> | |||
|
805 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
806 | ight Accent 5"/> | |||
|
807 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2 A= | |||
|
808 | ccent 5"/> | |||
|
809 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3 A= | |||
|
810 | ccent 5"/> | |||
|
811 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4 A= | |||
|
812 | ccent 5"/> | |||
|
813 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
814 | ark Accent 5"/> | |||
|
815 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
816 | olorful Accent 5"/> | |||
|
817 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
818 | olorful Accent 5"/> | |||
|
819 | <w:LsdException Locked=3D"false" Priority=3D"46" Name=3D"List Table 1 L= | |||
|
820 | ight Accent 6"/> | |||
|
821 | <w:LsdException Locked=3D"false" Priority=3D"47" Name=3D"List Table 2 A= | |||
|
822 | ccent 6"/> | |||
|
823 | <w:LsdException Locked=3D"false" Priority=3D"48" Name=3D"List Table 3 A= | |||
|
824 | ccent 6"/> | |||
|
825 | <w:LsdException Locked=3D"false" Priority=3D"49" Name=3D"List Table 4 A= | |||
|
826 | ccent 6"/> | |||
|
827 | <w:LsdException Locked=3D"false" Priority=3D"50" Name=3D"List Table 5 D= | |||
|
828 | ark Accent 6"/> | |||
|
829 | <w:LsdException Locked=3D"false" Priority=3D"51" Name=3D"List Table 6 C= | |||
|
830 | olorful Accent 6"/> | |||
|
831 | <w:LsdException Locked=3D"false" Priority=3D"52" Name=3D"List Table 7 C= | |||
|
832 | olorful Accent 6"/> | |||
|
833 | </w:LatentStyles> | |||
|
834 | </xml><![endif]--><style><!-- | |||
|
835 | /* Font Definitions */ | |||
|
836 | @font-face | |||
|
837 | {font-family:"Cambria Math"; | |||
|
838 | panose-1:2 4 5 3 5 4 6 3 2 4; | |||
|
839 | mso-font-charset:0; | |||
|
840 | mso-generic-font-family:roman; | |||
|
841 | mso-font-pitch:variable; | |||
|
842 | mso-font-signature:-536870145 1107305727 0 0 415 0;} | |||
|
843 | @font-face | |||
|
844 | {font-family:Calibri; | |||
|
845 | panose-1:2 15 5 2 2 2 4 3 2 4; | |||
|
846 | mso-font-alt:"Times New Roman"; | |||
|
847 | mso-font-charset:0; | |||
|
848 | mso-generic-font-family:swiss; | |||
|
849 | mso-font-pitch:variable; | |||
|
850 | mso-font-signature:-536870145 1073786111 1 0 415 0;} | |||
|
851 | /* Style Definitions */ | |||
|
852 | p.MsoNormal, li.MsoNormal, div.MsoNormal | |||
|
853 | {mso-style-unhide:no; | |||
|
854 | mso-style-qformat:yes; | |||
|
855 | mso-style-parent:""; | |||
|
856 | margin:0in; | |||
|
857 | margin-bottom:.0001pt; | |||
|
858 | mso-pagination:widow-orphan; | |||
|
859 | font-size:11.0pt; | |||
|
860 | font-family:"Calibri",sans-serif; | |||
|
861 | mso-ascii-font-family:Calibri; | |||
|
862 | mso-ascii-theme-font:minor-latin; | |||
|
863 | mso-fareast-font-family:Calibri; | |||
|
864 | mso-fareast-theme-font:minor-latin; | |||
|
865 | mso-hansi-font-family:Calibri; | |||
|
866 | mso-hansi-theme-font:minor-latin; | |||
|
867 | mso-bidi-font-family:"Times New Roman"; | |||
|
868 | mso-bidi-theme-font:minor-bidi;} | |||
|
869 | a:link, span.MsoHyperlink | |||
|
870 | {mso-style-noshow:yes; | |||
|
871 | mso-style-priority:99; | |||
|
872 | color:blue; | |||
|
873 | mso-themecolor:hyperlink; | |||
|
874 | text-decoration:underline; | |||
|
875 | text-underline:single;} | |||
|
876 | a:visited, span.MsoHyperlinkFollowed | |||
|
877 | {mso-style-noshow:yes; | |||
|
878 | mso-style-priority:99; | |||
|
879 | color:purple; | |||
|
880 | mso-themecolor:followedhyperlink; | |||
|
881 | text-decoration:underline; | |||
|
882 | text-underline:single;} | |||
|
883 | p.MsoPlainText, li.MsoPlainText, div.MsoPlainText | |||
|
884 | {mso-style-noshow:yes; | |||
|
885 | mso-style-priority:99; | |||
|
886 | mso-style-link:"Plain Text Char"; | |||
|
887 | margin:0in; | |||
|
888 | margin-bottom:.0001pt; | |||
|
889 | mso-pagination:widow-orphan; | |||
|
890 | font-size:11.0pt; | |||
|
891 | mso-bidi-font-size:10.5pt; | |||
|
892 | font-family:"Calibri",sans-serif; | |||
|
893 | mso-fareast-font-family:Calibri; | |||
|
894 | mso-fareast-theme-font:minor-latin; | |||
|
895 | mso-bidi-font-family:"Times New Roman"; | |||
|
896 | mso-bidi-theme-font:minor-bidi;} | |||
|
897 | span.PlainTextChar | |||
|
898 | {mso-style-name:"Plain Text Char"; | |||
|
899 | mso-style-noshow:yes; | |||
|
900 | mso-style-priority:99; | |||
|
901 | mso-style-unhide:no; | |||
|
902 | mso-style-locked:yes; | |||
|
903 | mso-style-link:"Plain Text"; | |||
|
904 | mso-bidi-font-size:10.5pt; | |||
|
905 | font-family:"Calibri",sans-serif; | |||
|
906 | mso-ascii-font-family:Calibri; | |||
|
907 | mso-hansi-font-family:Calibri;} | |||
|
908 | span.EmailStyle19 | |||
|
909 | {mso-style-type:personal-compose; | |||
|
910 | mso-style-noshow:yes; | |||
|
911 | mso-style-unhide:no;} | |||
|
912 | .MsoChpDefault | |||
|
913 | {mso-style-type:export-only; | |||
|
914 | mso-default-props:yes; | |||
|
915 | font-size:10.0pt; | |||
|
916 | mso-ansi-font-size:10.0pt; | |||
|
917 | mso-bidi-font-size:10.0pt; | |||
|
918 | font-family:"Calibri",sans-serif; | |||
|
919 | mso-ascii-font-family:Calibri; | |||
|
920 | mso-ascii-theme-font:minor-latin; | |||
|
921 | mso-fareast-font-family:Calibri; | |||
|
922 | mso-fareast-theme-font:minor-latin; | |||
|
923 | mso-hansi-font-family:Calibri; | |||
|
924 | mso-hansi-theme-font:minor-latin; | |||
|
925 | mso-bidi-font-family:"Times New Roman"; | |||
|
926 | mso-bidi-theme-font:minor-bidi;} | |||
|
927 | @page WordSection1 | |||
|
928 | {size:8.5in 11.0in; | |||
|
929 | margin:1.0in 1.0in 1.0in 1.0in; | |||
|
930 | mso-header-margin:.5in; | |||
|
931 | mso-footer-margin:.5in; | |||
|
932 | mso-paper-source:0;} | |||
|
933 | div.WordSection1 | |||
|
934 | {page:WordSection1;} | |||
|
935 | --></style><!--[if gte mso 10]><style>/* Style Definitions */ | |||
|
936 | table.MsoNormalTable | |||
|
937 | {mso-style-name:"Table Normal"; | |||
|
938 | mso-tstyle-rowband-size:0; | |||
|
939 | mso-tstyle-colband-size:0; | |||
|
940 | mso-style-noshow:yes; | |||
|
941 | mso-style-priority:99; | |||
|
942 | mso-style-parent:""; | |||
|
943 | mso-padding-alt:0in 5.4pt 0in 5.4pt; | |||
|
944 | mso-para-margin:0in; | |||
|
945 | mso-para-margin-bottom:.0001pt; | |||
|
946 | mso-pagination:widow-orphan; | |||
|
947 | font-size:10.0pt; | |||
|
948 | font-family:"Calibri",sans-serif; | |||
|
949 | mso-ascii-font-family:Calibri; | |||
|
950 | mso-ascii-theme-font:minor-latin; | |||
|
951 | mso-hansi-font-family:Calibri; | |||
|
952 | mso-hansi-theme-font:minor-latin;} | |||
|
953 | </style><![endif]--><!--[if gte mso 9]><xml> | |||
|
954 | <o:shapedefaults v:ext=3D"edit" spidmax=3D"1026" /> | |||
|
955 | </xml><![endif]--><!--[if gte mso 9]><xml> | |||
|
956 | <o:shapelayout v:ext=3D"edit"> | |||
|
957 | <o:idmap v:ext=3D"edit" data=3D"1" /> | |||
|
958 | </o:shapelayout></xml><![endif]--></head><body lang=3DEN-US link=3Dblue= | |||
|
959 | vlink=3Dpurple style=3D'tab-interval:.5in'><div class=3DWordSection1><= | |||
|
960 | p class=3DMsoPlainText>Simple, unadorned test email generated by Outloo= | |||
|
961 | k 2010. It is in HTML format, but no special formatting has been chosen= | |||
|
962 | . I=E2=80=99m going to save this as a draft and then manually drop it i= | |||
|
963 | nto the Inbox for scraping by Redmine 3.0.2.<o:p></o:p></p></div></body= | |||
|
964 | ></html> | |||
|
965 | ||||
|
966 | --Mark=_539924359269962179476-- |
@@ -0,0 +1,65 | |||||
|
1 | From: "John Smith" <jsmith@somenet.foo> | |||
|
2 | To: redmine <r@MYCOMPANYNAME.com> | |||
|
3 | Subject: Upgrade Redmine to 3.0.x | |||
|
4 | Thread-Topic: Upgrade Redmine to 3.0.x | |||
|
5 | Thread-Index: AQHQknBe94y5Or7Yl02JransMRF41p2Dv6Hu | |||
|
6 | Date: Tue, 19 May 2015 16:27:43 -0400 | |||
|
7 | Message-ID: <A2D341A8808F024CAFA63F1287B9929CF1BC440F@EMBX01.exch.local> | |||
|
8 | Accept-Language: en-US | |||
|
9 | Content-Language: en-US | |||
|
10 | X-MS-Exchange-Organization-AuthAs: Internal | |||
|
11 | X-MS-Exchange-Organization-AuthMechanism: 04 | |||
|
12 | X-MS-Exchange-Organization-AuthSource: EHUB01.exch.local | |||
|
13 | X-MS-Has-Attach: | |||
|
14 | X-MS-Exchange-Organization-SCL: -1 | |||
|
15 | X-MS-TNEF-Correlator: | |||
|
16 | Content-Type: text/html; charset="iso-8859-1" | |||
|
17 | Content-Transfer-Encoding: quoted-printable | |||
|
18 | MIME-Version: 1.0 | |||
|
19 | ||||
|
20 | <html dir=3D"ltr"> | |||
|
21 | <head> | |||
|
22 | <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Diso-8859-= | |||
|
23 | 1"> | |||
|
24 | <style>=0A= | |||
|
25 | <!--=0A= | |||
|
26 | body=0A= | |||
|
27 | {font-family:Verdana,sans-serif;=0A= | |||
|
28 | font-size:0.8em;=0A= | |||
|
29 | color:#484848}=0A= | |||
|
30 | h1, h2, h3=0A= | |||
|
31 | {font-family:"Trebuchet MS",Verdana,sans-serif;=0A= | |||
|
32 | margin:0px}=0A= | |||
|
33 | h1=0A= | |||
|
34 | {font-size:1.2em}=0A= | |||
|
35 | h2, h3=0A= | |||
|
36 | {font-size:1.1em}=0A= | |||
|
37 | a, a:link, a:visited=0A= | |||
|
38 | {color:#2A5685}=0A= | |||
|
39 | a:hover, a:active=0A= | |||
|
40 | {color:#c61a1a}=0A= | |||
|
41 | fieldset.attachments=0A= | |||
|
42 | {border-width:1px 0 0 0}=0A= | |||
|
43 | hr=0A= | |||
|
44 | {width:100%;=0A= | |||
|
45 | height:1px;=0A= | |||
|
46 | background:#ccc;=0A= | |||
|
47 | border:0}=0A= | |||
|
48 | span.footer=0A= | |||
|
49 | {font-size:0.8em;=0A= | |||
|
50 | font-style:italic}=0A= | |||
|
51 | -->=0A= | |||
|
52 | </style><style id=3D"owaParaStyle" type=3D"text/css">P {margin-top:0;margin= | |||
|
53 | -bottom:0;}</style> | |||
|
54 | </head> | |||
|
55 | <body ocsi=3D"0" fpstyle=3D"1"> | |||
|
56 | <div style=3D"direction: ltr;font-family: Tahoma;color: #000000;font-size: = | |||
|
57 | 10pt;">A mess.<br> | |||
|
58 | <div><br> | |||
|
59 | <div style=3D"font-family:Tahoma; font-size:13px">--Geoff Maciolek<br> | |||
|
60 | MYCOMPANYNAME, LLC<br> | |||
|
61 | </div> | |||
|
62 | </div> | |||
|
63 | </div> | |||
|
64 | </body> | |||
|
65 | </html> No newline at end of file |
@@ -0,0 +1,30 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
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. | |||
|
17 | ||||
|
18 | require File.expand_path('../../../../../test_helper', __FILE__) | |||
|
19 | ||||
|
20 | class Redmine::WikiFormatting::HtmlParserTest < ActiveSupport::TestCase | |||
|
21 | ||||
|
22 | def setup | |||
|
23 | @parser = Redmine::WikiFormatting::HtmlParser | |||
|
24 | end | |||
|
25 | ||||
|
26 | def test_convert_line_breaks | |||
|
27 | assert_equal "A html snippet with\na new line.", | |||
|
28 | @parser.to_text('<p>A html snippet with<br>a new line.</p>') | |||
|
29 | end | |||
|
30 | end |
@@ -0,0 +1,30 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
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. | |||
|
17 | ||||
|
18 | require File.expand_path('../../../../../test_helper', __FILE__) | |||
|
19 | ||||
|
20 | class Redmine::WikiFormatting::MarkdownHtmlParserTest < ActiveSupport::TestCase | |||
|
21 | ||||
|
22 | def setup | |||
|
23 | @parser = Redmine::WikiFormatting::Markdown::HtmlParser | |||
|
24 | end | |||
|
25 | ||||
|
26 | def test_should_convert_tags | |||
|
27 | assert_equal 'A **simple** html snippet.', | |||
|
28 | @parser.to_text('<p>A <b>simple</b> html snippet.</p>') | |||
|
29 | end | |||
|
30 | end |
@@ -0,0 +1,30 | |||||
|
1 | # Redmine - project management software | |||
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |||
|
3 | # | |||
|
4 | # This program is free software; you can redistribute it and/or | |||
|
5 | # modify it under the terms of the GNU General Public License | |||
|
6 | # as published by the Free Software Foundation; either version 2 | |||
|
7 | # of the License, or (at your option) any later version. | |||
|
8 | # | |||
|
9 | # This program is distributed in the hope that it will be useful, | |||
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
12 | # GNU General Public License for more details. | |||
|
13 | # | |||
|
14 | # You should have received a copy of the GNU General Public License | |||
|
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. | |||
|
17 | ||||
|
18 | require File.expand_path('../../../../../test_helper', __FILE__) | |||
|
19 | ||||
|
20 | class Redmine::WikiFormatting::TextileHtmlParserTest < ActiveSupport::TestCase | |||
|
21 | ||||
|
22 | def setup | |||
|
23 | @parser = Redmine::WikiFormatting::Textile::HtmlParser | |||
|
24 | end | |||
|
25 | ||||
|
26 | def test_should_convert_tags | |||
|
27 | assert_equal 'A *simple* html snippet.', | |||
|
28 | @parser.to_text('<p>A <b>simple</b> html snippet.</p>') | |||
|
29 | end | |||
|
30 | end |
@@ -1,110 +1,111 | |||||
1 | source 'https://rubygems.org' |
|
1 | source 'https://rubygems.org' | |
2 |
|
2 | |||
3 | if Gem::Version.new(Bundler::VERSION) < Gem::Version.new('1.5.0') |
|
3 | if Gem::Version.new(Bundler::VERSION) < Gem::Version.new('1.5.0') | |
4 | abort "Redmine requires Bundler 1.5.0 or higher (you're using #{Bundler::VERSION}).\nPlease update with 'gem update bundler'." |
|
4 | abort "Redmine requires Bundler 1.5.0 or higher (you're using #{Bundler::VERSION}).\nPlease update with 'gem update bundler'." | |
5 | end |
|
5 | end | |
6 |
|
6 | |||
7 | gem "rails", "4.2.1" |
|
7 | gem "rails", "4.2.1" | |
8 | gem "jquery-rails", "~> 3.1.1" |
|
8 | gem "jquery-rails", "~> 3.1.1" | |
9 | gem "coderay", "~> 1.1.0" |
|
9 | gem "coderay", "~> 1.1.0" | |
10 | gem "builder", ">= 3.0.4" |
|
10 | gem "builder", ">= 3.0.4" | |
11 | gem "request_store", "1.0.5" |
|
11 | gem "request_store", "1.0.5" | |
12 | gem "mime-types" |
|
12 | gem "mime-types" | |
13 | gem "protected_attributes" |
|
13 | gem "protected_attributes" | |
14 | gem "actionpack-action_caching" |
|
14 | gem "actionpack-action_caching" | |
15 | gem "actionpack-xml_parser" |
|
15 | gem "actionpack-xml_parser" | |
|
16 | gem "loofah", "~> 2.0" | |||
16 |
|
17 | |||
17 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem |
|
18 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem | |
18 | gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin, :jruby] |
|
19 | gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin, :jruby] | |
19 | gem "rbpdf", "~> 1.18.5" |
|
20 | gem "rbpdf", "~> 1.18.5" | |
20 |
|
21 | |||
21 | # Optional gem for LDAP authentication |
|
22 | # Optional gem for LDAP authentication | |
22 | group :ldap do |
|
23 | group :ldap do | |
23 | gem "net-ldap", "~> 0.3.1" |
|
24 | gem "net-ldap", "~> 0.3.1" | |
24 | end |
|
25 | end | |
25 |
|
26 | |||
26 | # Optional gem for OpenID authentication |
|
27 | # Optional gem for OpenID authentication | |
27 | group :openid do |
|
28 | group :openid do | |
28 | gem "ruby-openid", "~> 2.3.0", :require => "openid" |
|
29 | gem "ruby-openid", "~> 2.3.0", :require => "openid" | |
29 | gem "rack-openid" |
|
30 | gem "rack-openid" | |
30 | end |
|
31 | end | |
31 |
|
32 | |||
32 | platforms :mri, :mingw, :x64_mingw do |
|
33 | platforms :mri, :mingw, :x64_mingw do | |
33 | # Optional gem for exporting the gantt to a PNG file, not supported with jruby |
|
34 | # Optional gem for exporting the gantt to a PNG file, not supported with jruby | |
34 | group :rmagick do |
|
35 | group :rmagick do | |
35 | gem "rmagick", ">= 2.14.0" |
|
36 | gem "rmagick", ">= 2.14.0" | |
36 | end |
|
37 | end | |
37 |
|
38 | |||
38 | # Optional Markdown support, not for JRuby |
|
39 | # Optional Markdown support, not for JRuby | |
39 | group :markdown do |
|
40 | group :markdown do | |
40 | gem "redcarpet", "~> 3.1.2" |
|
41 | gem "redcarpet", "~> 3.1.2" | |
41 | end |
|
42 | end | |
42 | end |
|
43 | end | |
43 |
|
44 | |||
44 | platforms :jruby do |
|
45 | platforms :jruby do | |
45 | # jruby-openssl is bundled with JRuby 1.7.0 |
|
46 | # jruby-openssl is bundled with JRuby 1.7.0 | |
46 | gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0' |
|
47 | gem "jruby-openssl" if Object.const_defined?(:JRUBY_VERSION) && JRUBY_VERSION < '1.7.0' | |
47 | gem "activerecord-jdbc-adapter", "~> 1.3.2" |
|
48 | gem "activerecord-jdbc-adapter", "~> 1.3.2" | |
48 | end |
|
49 | end | |
49 |
|
50 | |||
50 | # Include database gems for the adapters found in the database |
|
51 | # Include database gems for the adapters found in the database | |
51 | # configuration file |
|
52 | # configuration file | |
52 | require 'erb' |
|
53 | require 'erb' | |
53 | require 'yaml' |
|
54 | require 'yaml' | |
54 | database_file = File.join(File.dirname(__FILE__), "config/database.yml") |
|
55 | database_file = File.join(File.dirname(__FILE__), "config/database.yml") | |
55 | if File.exist?(database_file) |
|
56 | if File.exist?(database_file) | |
56 | database_config = YAML::load(ERB.new(IO.read(database_file)).result) |
|
57 | database_config = YAML::load(ERB.new(IO.read(database_file)).result) | |
57 | adapters = database_config.values.map {|c| c['adapter']}.compact.uniq |
|
58 | adapters = database_config.values.map {|c| c['adapter']}.compact.uniq | |
58 | if adapters.any? |
|
59 | if adapters.any? | |
59 | adapters.each do |adapter| |
|
60 | adapters.each do |adapter| | |
60 | case adapter |
|
61 | case adapter | |
61 | when 'mysql2' |
|
62 | when 'mysql2' | |
62 | gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw, :x64_mingw] |
|
63 | gem "mysql2", "~> 0.3.11", :platforms => [:mri, :mingw, :x64_mingw] | |
63 | gem "activerecord-jdbcmysql-adapter", :platforms => :jruby |
|
64 | gem "activerecord-jdbcmysql-adapter", :platforms => :jruby | |
64 | when 'mysql' |
|
65 | when 'mysql' | |
65 | gem "activerecord-jdbcmysql-adapter", :platforms => :jruby |
|
66 | gem "activerecord-jdbcmysql-adapter", :platforms => :jruby | |
66 | when /postgresql/ |
|
67 | when /postgresql/ | |
67 | gem "pg", "~> 0.18.1", :platforms => [:mri, :mingw, :x64_mingw] |
|
68 | gem "pg", "~> 0.18.1", :platforms => [:mri, :mingw, :x64_mingw] | |
68 | gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby |
|
69 | gem "activerecord-jdbcpostgresql-adapter", :platforms => :jruby | |
69 | when /sqlite3/ |
|
70 | when /sqlite3/ | |
70 | gem "sqlite3", :platforms => [:mri, :mingw, :x64_mingw] |
|
71 | gem "sqlite3", :platforms => [:mri, :mingw, :x64_mingw] | |
71 | gem "jdbc-sqlite3", "< 3.8", :platforms => :jruby |
|
72 | gem "jdbc-sqlite3", "< 3.8", :platforms => :jruby | |
72 | gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby |
|
73 | gem "activerecord-jdbcsqlite3-adapter", :platforms => :jruby | |
73 | when /sqlserver/ |
|
74 | when /sqlserver/ | |
74 | gem "tiny_tds", "~> 0.6.2", :platforms => [:mri, :mingw, :x64_mingw] |
|
75 | gem "tiny_tds", "~> 0.6.2", :platforms => [:mri, :mingw, :x64_mingw] | |
75 | gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw, :x64_mingw] |
|
76 | gem "activerecord-sqlserver-adapter", :platforms => [:mri, :mingw, :x64_mingw] | |
76 | else |
|
77 | else | |
77 | warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems") |
|
78 | warn("Unknown database adapter `#{adapter}` found in config/database.yml, use Gemfile.local to load your own database gems") | |
78 | end |
|
79 | end | |
79 | end |
|
80 | end | |
80 | else |
|
81 | else | |
81 | warn("No adapter found in config/database.yml, please configure it first") |
|
82 | warn("No adapter found in config/database.yml, please configure it first") | |
82 | end |
|
83 | end | |
83 | else |
|
84 | else | |
84 | warn("Please configure your config/database.yml first") |
|
85 | warn("Please configure your config/database.yml first") | |
85 | end |
|
86 | end | |
86 |
|
87 | |||
87 | group :development do |
|
88 | group :development do | |
88 | gem "rdoc", ">= 2.4.2" |
|
89 | gem "rdoc", ">= 2.4.2" | |
89 | gem "yard" |
|
90 | gem "yard" | |
90 | end |
|
91 | end | |
91 |
|
92 | |||
92 | group :test do |
|
93 | group :test do | |
93 | gem "minitest" |
|
94 | gem "minitest" | |
94 | gem "rails-dom-testing" |
|
95 | gem "rails-dom-testing" | |
95 | gem "mocha" |
|
96 | gem "mocha" | |
96 | gem "simplecov", "~> 0.9.1", :require => false |
|
97 | gem "simplecov", "~> 0.9.1", :require => false | |
97 | # For running UI tests |
|
98 | # For running UI tests | |
98 | gem "capybara" |
|
99 | gem "capybara" | |
99 | gem "selenium-webdriver" |
|
100 | gem "selenium-webdriver" | |
100 | end |
|
101 | end | |
101 |
|
102 | |||
102 | local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") |
|
103 | local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local") | |
103 | if File.exists?(local_gemfile) |
|
104 | if File.exists?(local_gemfile) | |
104 | eval_gemfile local_gemfile |
|
105 | eval_gemfile local_gemfile | |
105 | end |
|
106 | end | |
106 |
|
107 | |||
107 | # Load plugins' Gemfiles |
|
108 | # Load plugins' Gemfiles | |
108 | Dir.glob File.expand_path("../plugins/*/{Gemfile,PluginGemfile}", __FILE__) do |file| |
|
109 | Dir.glob File.expand_path("../plugins/*/{Gemfile,PluginGemfile}", __FILE__) do |file| | |
109 | eval_gemfile file |
|
110 | eval_gemfile file | |
110 | end |
|
111 | end |
@@ -1,556 +1,558 | |||||
1 | # Redmine - project management software |
|
1 | # Redmine - project management software | |
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
14 | # You should have received a copy of the GNU General Public License | |
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 | class MailHandler < ActionMailer::Base | |
19 | include ActionView::Helpers::SanitizeHelper |
|
19 | include ActionView::Helpers::SanitizeHelper | |
20 | include Redmine::I18n |
|
20 | include Redmine::I18n | |
21 |
|
21 | |||
22 | class UnauthorizedAction < StandardError; end |
|
22 | class UnauthorizedAction < StandardError; end | |
23 | class MissingInformation < StandardError; end |
|
23 | class MissingInformation < StandardError; end | |
24 |
|
24 | |||
25 | attr_reader :email, :user, :handler_options |
|
25 | attr_reader :email, :user, :handler_options | |
26 |
|
26 | |||
27 | def self.receive(raw_mail, options={}) |
|
27 | def self.receive(raw_mail, options={}) | |
28 | options = options.deep_dup |
|
28 | options = options.deep_dup | |
29 |
|
29 | |||
30 | options[:issue] ||= {} |
|
30 | options[:issue] ||= {} | |
31 |
|
31 | |||
32 | if options[:allow_override].is_a?(String) |
|
32 | if options[:allow_override].is_a?(String) | |
33 | options[:allow_override] = options[:allow_override].split(',').collect(&:strip) |
|
33 | options[:allow_override] = options[:allow_override].split(',').collect(&:strip) | |
34 | end |
|
34 | end | |
35 | options[:allow_override] ||= [] |
|
35 | options[:allow_override] ||= [] | |
36 | # Project needs to be overridable if not specified |
|
36 | # Project needs to be overridable if not specified | |
37 | options[:allow_override] << 'project' unless options[:issue].has_key?(:project) |
|
37 | options[:allow_override] << 'project' unless options[:issue].has_key?(:project) | |
38 | # Status overridable by default |
|
38 | # Status overridable by default | |
39 | options[:allow_override] << 'status' unless options[:issue].has_key?(:status) |
|
39 | options[:allow_override] << 'status' unless options[:issue].has_key?(:status) | |
40 |
|
40 | |||
41 | options[:no_account_notice] = (options[:no_account_notice].to_s == '1') |
|
41 | options[:no_account_notice] = (options[:no_account_notice].to_s == '1') | |
42 | options[:no_notification] = (options[:no_notification].to_s == '1') |
|
42 | options[:no_notification] = (options[:no_notification].to_s == '1') | |
43 | options[:no_permission_check] = (options[:no_permission_check].to_s == '1') |
|
43 | options[:no_permission_check] = (options[:no_permission_check].to_s == '1') | |
44 |
|
44 | |||
45 | raw_mail.force_encoding('ASCII-8BIT') |
|
45 | raw_mail.force_encoding('ASCII-8BIT') | |
46 |
|
46 | |||
47 | ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload| |
|
47 | ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload| | |
48 | mail = Mail.new(raw_mail) |
|
48 | mail = Mail.new(raw_mail) | |
49 | set_payload_for_mail(payload, mail) |
|
49 | set_payload_for_mail(payload, mail) | |
50 | new.receive(mail, options) |
|
50 | new.receive(mail, options) | |
51 | end |
|
51 | end | |
52 | end |
|
52 | end | |
53 |
|
53 | |||
54 | # Receives an email and rescues any exception |
|
54 | # Receives an email and rescues any exception | |
55 | def self.safe_receive(*args) |
|
55 | def self.safe_receive(*args) | |
56 | receive(*args) |
|
56 | receive(*args) | |
57 | rescue Exception => e |
|
57 | rescue Exception => e | |
58 | logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger |
|
58 | logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger | |
59 | return false |
|
59 | return false | |
60 | end |
|
60 | end | |
61 |
|
61 | |||
62 | # Extracts MailHandler options from environment variables |
|
62 | # Extracts MailHandler options from environment variables | |
63 | # Use when receiving emails with rake tasks |
|
63 | # Use when receiving emails with rake tasks | |
64 | def self.extract_options_from_env(env) |
|
64 | def self.extract_options_from_env(env) | |
65 | options = {:issue => {}} |
|
65 | options = {:issue => {}} | |
66 | %w(project status tracker category priority).each do |option| |
|
66 | %w(project status tracker category priority).each do |option| | |
67 | options[:issue][option.to_sym] = env[option] if env[option] |
|
67 | options[:issue][option.to_sym] = env[option] if env[option] | |
68 | end |
|
68 | end | |
69 | %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option| |
|
69 | %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option| | |
70 | options[option.to_sym] = env[option] if env[option] |
|
70 | options[option.to_sym] = env[option] if env[option] | |
71 | end |
|
71 | end | |
72 | if env['private'] |
|
72 | if env['private'] | |
73 | options[:issue][:is_private] = '1' |
|
73 | options[:issue][:is_private] = '1' | |
74 | end |
|
74 | end | |
75 | options |
|
75 | options | |
76 | end |
|
76 | end | |
77 |
|
77 | |||
78 | def logger |
|
78 | def logger | |
79 | Rails.logger |
|
79 | Rails.logger | |
80 | end |
|
80 | end | |
81 |
|
81 | |||
82 | cattr_accessor :ignored_emails_headers |
|
82 | cattr_accessor :ignored_emails_headers | |
83 | self.ignored_emails_headers = { |
|
83 | self.ignored_emails_headers = { | |
84 | 'Auto-Submitted' => /\Aauto-(replied|generated)/, |
|
84 | 'Auto-Submitted' => /\Aauto-(replied|generated)/, | |
85 | 'X-Autoreply' => 'yes' |
|
85 | 'X-Autoreply' => 'yes' | |
86 | } |
|
86 | } | |
87 |
|
87 | |||
88 | # Processes incoming emails |
|
88 | # Processes incoming emails | |
89 | # Returns the created object (eg. an issue, a message) or false |
|
89 | # Returns the created object (eg. an issue, a message) or false | |
90 | def receive(email, options={}) |
|
90 | def receive(email, options={}) | |
91 | @email = email |
|
91 | @email = email | |
92 | @handler_options = options |
|
92 | @handler_options = options | |
93 | sender_email = email.from.to_a.first.to_s.strip |
|
93 | sender_email = email.from.to_a.first.to_s.strip | |
94 | # Ignore emails received from the application emission address to avoid hell cycles |
|
94 | # Ignore emails received from the application emission address to avoid hell cycles | |
95 | if sender_email.downcase == Setting.mail_from.to_s.strip.downcase |
|
95 | if sender_email.downcase == Setting.mail_from.to_s.strip.downcase | |
96 | if logger |
|
96 | if logger | |
97 | logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" |
|
97 | logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" | |
98 | end |
|
98 | end | |
99 | return false |
|
99 | return false | |
100 | end |
|
100 | end | |
101 | # Ignore auto generated emails |
|
101 | # Ignore auto generated emails | |
102 | self.class.ignored_emails_headers.each do |key, ignored_value| |
|
102 | self.class.ignored_emails_headers.each do |key, ignored_value| | |
103 | value = email.header[key] |
|
103 | value = email.header[key] | |
104 | if value |
|
104 | if value | |
105 | value = value.to_s.downcase |
|
105 | value = value.to_s.downcase | |
106 | if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value |
|
106 | if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value | |
107 | if logger |
|
107 | if logger | |
108 | logger.info "MailHandler: ignoring email with #{key}:#{value} header" |
|
108 | logger.info "MailHandler: ignoring email with #{key}:#{value} header" | |
109 | end |
|
109 | end | |
110 | return false |
|
110 | return false | |
111 | end |
|
111 | end | |
112 | end |
|
112 | end | |
113 | end |
|
113 | end | |
114 | @user = User.find_by_mail(sender_email) if sender_email.present? |
|
114 | @user = User.find_by_mail(sender_email) if sender_email.present? | |
115 | if @user && !@user.active? |
|
115 | if @user && !@user.active? | |
116 | if logger |
|
116 | if logger | |
117 | logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" |
|
117 | logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" | |
118 | end |
|
118 | end | |
119 | return false |
|
119 | return false | |
120 | end |
|
120 | end | |
121 | if @user.nil? |
|
121 | if @user.nil? | |
122 | # Email was submitted by an unknown user |
|
122 | # Email was submitted by an unknown user | |
123 | case handler_options[:unknown_user] |
|
123 | case handler_options[:unknown_user] | |
124 | when 'accept' |
|
124 | when 'accept' | |
125 | @user = User.anonymous |
|
125 | @user = User.anonymous | |
126 | when 'create' |
|
126 | when 'create' | |
127 | @user = create_user_from_email |
|
127 | @user = create_user_from_email | |
128 | if @user |
|
128 | if @user | |
129 | if logger |
|
129 | if logger | |
130 | logger.info "MailHandler: [#{@user.login}] account created" |
|
130 | logger.info "MailHandler: [#{@user.login}] account created" | |
131 | end |
|
131 | end | |
132 | add_user_to_group(handler_options[:default_group]) |
|
132 | add_user_to_group(handler_options[:default_group]) | |
133 | unless handler_options[:no_account_notice] |
|
133 | unless handler_options[:no_account_notice] | |
134 | Mailer.account_information(@user, @user.password).deliver |
|
134 | Mailer.account_information(@user, @user.password).deliver | |
135 | end |
|
135 | end | |
136 | else |
|
136 | else | |
137 | if logger |
|
137 | if logger | |
138 | logger.error "MailHandler: could not create account for [#{sender_email}]" |
|
138 | logger.error "MailHandler: could not create account for [#{sender_email}]" | |
139 | end |
|
139 | end | |
140 | return false |
|
140 | return false | |
141 | end |
|
141 | end | |
142 | else |
|
142 | else | |
143 | # Default behaviour, emails from unknown users are ignored |
|
143 | # Default behaviour, emails from unknown users are ignored | |
144 | if logger |
|
144 | if logger | |
145 | logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" |
|
145 | logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" | |
146 | end |
|
146 | end | |
147 | return false |
|
147 | return false | |
148 | end |
|
148 | end | |
149 | end |
|
149 | end | |
150 | User.current = @user |
|
150 | User.current = @user | |
151 | dispatch |
|
151 | dispatch | |
152 | end |
|
152 | end | |
153 |
|
153 | |||
154 | private |
|
154 | private | |
155 |
|
155 | |||
156 | MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@} |
|
156 | MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@} | |
157 | ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]} |
|
157 | ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]} | |
158 | MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]} |
|
158 | MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]} | |
159 |
|
159 | |||
160 | def dispatch |
|
160 | def dispatch | |
161 | headers = [email.in_reply_to, email.references].flatten.compact |
|
161 | headers = [email.in_reply_to, email.references].flatten.compact | |
162 | subject = email.subject.to_s |
|
162 | subject = email.subject.to_s | |
163 | if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE} |
|
163 | if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE} | |
164 | klass, object_id = $1, $2.to_i |
|
164 | klass, object_id = $1, $2.to_i | |
165 | method_name = "receive_#{klass}_reply" |
|
165 | method_name = "receive_#{klass}_reply" | |
166 | if self.class.private_instance_methods.collect(&:to_s).include?(method_name) |
|
166 | if self.class.private_instance_methods.collect(&:to_s).include?(method_name) | |
167 | send method_name, object_id |
|
167 | send method_name, object_id | |
168 | else |
|
168 | else | |
169 | # ignoring it |
|
169 | # ignoring it | |
170 | end |
|
170 | end | |
171 | elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE) |
|
171 | elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE) | |
172 | receive_issue_reply(m[1].to_i) |
|
172 | receive_issue_reply(m[1].to_i) | |
173 | elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE) |
|
173 | elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE) | |
174 | receive_message_reply(m[1].to_i) |
|
174 | receive_message_reply(m[1].to_i) | |
175 | else |
|
175 | else | |
176 | dispatch_to_default |
|
176 | dispatch_to_default | |
177 | end |
|
177 | end | |
178 | rescue ActiveRecord::RecordInvalid => e |
|
178 | rescue ActiveRecord::RecordInvalid => e | |
179 | # TODO: send a email to the user |
|
179 | # TODO: send a email to the user | |
180 | logger.error e.message if logger |
|
180 | logger.error e.message if logger | |
181 | false |
|
181 | false | |
182 | rescue MissingInformation => e |
|
182 | rescue MissingInformation => e | |
183 | logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger |
|
183 | logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger | |
184 | false |
|
184 | false | |
185 | rescue UnauthorizedAction => e |
|
185 | rescue UnauthorizedAction => e | |
186 | logger.error "MailHandler: unauthorized attempt from #{user}" if logger |
|
186 | logger.error "MailHandler: unauthorized attempt from #{user}" if logger | |
187 | false |
|
187 | false | |
188 | end |
|
188 | end | |
189 |
|
189 | |||
190 | def dispatch_to_default |
|
190 | def dispatch_to_default | |
191 | receive_issue |
|
191 | receive_issue | |
192 | end |
|
192 | end | |
193 |
|
193 | |||
194 | # Creates a new issue |
|
194 | # Creates a new issue | |
195 | def receive_issue |
|
195 | def receive_issue | |
196 | project = target_project |
|
196 | project = target_project | |
197 | # check permission |
|
197 | # check permission | |
198 | unless handler_options[:no_permission_check] |
|
198 | unless handler_options[:no_permission_check] | |
199 | raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) |
|
199 | raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) | |
200 | end |
|
200 | end | |
201 |
|
201 | |||
202 | issue = Issue.new(:author => user, :project => project) |
|
202 | issue = Issue.new(:author => user, :project => project) | |
203 | issue.safe_attributes = issue_attributes_from_keywords(issue) |
|
203 | issue.safe_attributes = issue_attributes_from_keywords(issue) | |
204 | issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} |
|
204 | issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} | |
205 | issue.subject = cleaned_up_subject |
|
205 | issue.subject = cleaned_up_subject | |
206 | if issue.subject.blank? |
|
206 | if issue.subject.blank? | |
207 | issue.subject = '(no subject)' |
|
207 | issue.subject = '(no subject)' | |
208 | end |
|
208 | end | |
209 | issue.description = cleaned_up_text_body |
|
209 | issue.description = cleaned_up_text_body | |
210 | issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date? |
|
210 | issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date? | |
211 | issue.is_private = (handler_options[:issue][:is_private] == '1') |
|
211 | issue.is_private = (handler_options[:issue][:is_private] == '1') | |
212 |
|
212 | |||
213 | # add To and Cc as watchers before saving so the watchers can reply to Redmine |
|
213 | # add To and Cc as watchers before saving so the watchers can reply to Redmine | |
214 | add_watchers(issue) |
|
214 | add_watchers(issue) | |
215 | issue.save! |
|
215 | issue.save! | |
216 | add_attachments(issue) |
|
216 | add_attachments(issue) | |
217 | logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger |
|
217 | logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger | |
218 | issue |
|
218 | issue | |
219 | end |
|
219 | end | |
220 |
|
220 | |||
221 | # Adds a note to an existing issue |
|
221 | # Adds a note to an existing issue | |
222 | def receive_issue_reply(issue_id, from_journal=nil) |
|
222 | def receive_issue_reply(issue_id, from_journal=nil) | |
223 | issue = Issue.find_by_id(issue_id) |
|
223 | issue = Issue.find_by_id(issue_id) | |
224 | return unless issue |
|
224 | return unless issue | |
225 | # check permission |
|
225 | # check permission | |
226 | unless handler_options[:no_permission_check] |
|
226 | unless handler_options[:no_permission_check] | |
227 | unless user.allowed_to?(:add_issue_notes, issue.project) || |
|
227 | unless user.allowed_to?(:add_issue_notes, issue.project) || | |
228 | user.allowed_to?(:edit_issues, issue.project) |
|
228 | user.allowed_to?(:edit_issues, issue.project) | |
229 | raise UnauthorizedAction |
|
229 | raise UnauthorizedAction | |
230 | end |
|
230 | end | |
231 | end |
|
231 | end | |
232 |
|
232 | |||
233 | # ignore CLI-supplied defaults for new issues |
|
233 | # ignore CLI-supplied defaults for new issues | |
234 | handler_options[:issue].clear |
|
234 | handler_options[:issue].clear | |
235 |
|
235 | |||
236 | journal = issue.init_journal(user) |
|
236 | journal = issue.init_journal(user) | |
237 | if from_journal && from_journal.private_notes? |
|
237 | if from_journal && from_journal.private_notes? | |
238 | # If the received email was a reply to a private note, make the added note private |
|
238 | # If the received email was a reply to a private note, make the added note private | |
239 | issue.private_notes = true |
|
239 | issue.private_notes = true | |
240 | end |
|
240 | end | |
241 | issue.safe_attributes = issue_attributes_from_keywords(issue) |
|
241 | issue.safe_attributes = issue_attributes_from_keywords(issue) | |
242 | issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} |
|
242 | issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} | |
243 | journal.notes = cleaned_up_text_body |
|
243 | journal.notes = cleaned_up_text_body | |
244 | add_attachments(issue) |
|
244 | add_attachments(issue) | |
245 | issue.save! |
|
245 | issue.save! | |
246 | if logger |
|
246 | if logger | |
247 | logger.info "MailHandler: issue ##{issue.id} updated by #{user}" |
|
247 | logger.info "MailHandler: issue ##{issue.id} updated by #{user}" | |
248 | end |
|
248 | end | |
249 | journal |
|
249 | journal | |
250 | end |
|
250 | end | |
251 |
|
251 | |||
252 | # Reply will be added to the issue |
|
252 | # Reply will be added to the issue | |
253 | def receive_journal_reply(journal_id) |
|
253 | def receive_journal_reply(journal_id) | |
254 | journal = Journal.find_by_id(journal_id) |
|
254 | journal = Journal.find_by_id(journal_id) | |
255 | if journal && journal.journalized_type == 'Issue' |
|
255 | if journal && journal.journalized_type == 'Issue' | |
256 | receive_issue_reply(journal.journalized_id, journal) |
|
256 | receive_issue_reply(journal.journalized_id, journal) | |
257 | end |
|
257 | end | |
258 | end |
|
258 | end | |
259 |
|
259 | |||
260 | # Receives a reply to a forum message |
|
260 | # Receives a reply to a forum message | |
261 | def receive_message_reply(message_id) |
|
261 | def receive_message_reply(message_id) | |
262 | message = Message.find_by_id(message_id) |
|
262 | message = Message.find_by_id(message_id) | |
263 | if message |
|
263 | if message | |
264 | message = message.root |
|
264 | message = message.root | |
265 |
|
265 | |||
266 | unless handler_options[:no_permission_check] |
|
266 | unless handler_options[:no_permission_check] | |
267 | raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) |
|
267 | raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project) | |
268 | end |
|
268 | end | |
269 |
|
269 | |||
270 | if !message.locked? |
|
270 | if !message.locked? | |
271 | reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, |
|
271 | reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip, | |
272 | :content => cleaned_up_text_body) |
|
272 | :content => cleaned_up_text_body) | |
273 | reply.author = user |
|
273 | reply.author = user | |
274 | reply.board = message.board |
|
274 | reply.board = message.board | |
275 | message.children << reply |
|
275 | message.children << reply | |
276 | add_attachments(reply) |
|
276 | add_attachments(reply) | |
277 | reply |
|
277 | reply | |
278 | else |
|
278 | else | |
279 | if logger |
|
279 | if logger | |
280 | logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" |
|
280 | logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" | |
281 | end |
|
281 | end | |
282 | end |
|
282 | end | |
283 | end |
|
283 | end | |
284 | end |
|
284 | end | |
285 |
|
285 | |||
286 | def add_attachments(obj) |
|
286 | def add_attachments(obj) | |
287 | if email.attachments && email.attachments.any? |
|
287 | if email.attachments && email.attachments.any? | |
288 | email.attachments.each do |attachment| |
|
288 | email.attachments.each do |attachment| | |
289 | next unless accept_attachment?(attachment) |
|
289 | next unless accept_attachment?(attachment) | |
290 | obj.attachments << Attachment.create(:container => obj, |
|
290 | obj.attachments << Attachment.create(:container => obj, | |
291 | :file => attachment.decoded, |
|
291 | :file => attachment.decoded, | |
292 | :filename => attachment.filename, |
|
292 | :filename => attachment.filename, | |
293 | :author => user, |
|
293 | :author => user, | |
294 | :content_type => attachment.mime_type) |
|
294 | :content_type => attachment.mime_type) | |
295 | end |
|
295 | end | |
296 | end |
|
296 | end | |
297 | end |
|
297 | end | |
298 |
|
298 | |||
299 | # Returns false if the +attachment+ of the incoming email should be ignored |
|
299 | # Returns false if the +attachment+ of the incoming email should be ignored | |
300 | def accept_attachment?(attachment) |
|
300 | def accept_attachment?(attachment) | |
301 | @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?) |
|
301 | @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?) | |
302 | @excluded.each do |pattern| |
|
302 | @excluded.each do |pattern| | |
303 | regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i |
|
303 | regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i | |
304 | if attachment.filename.to_s =~ regexp |
|
304 | if attachment.filename.to_s =~ regexp | |
305 | logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}" |
|
305 | logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}" | |
306 | return false |
|
306 | return false | |
307 | end |
|
307 | end | |
308 | end |
|
308 | end | |
309 | true |
|
309 | true | |
310 | end |
|
310 | end | |
311 |
|
311 | |||
312 | # Adds To and Cc as watchers of the given object if the sender has the |
|
312 | # Adds To and Cc as watchers of the given object if the sender has the | |
313 | # appropriate permission |
|
313 | # appropriate permission | |
314 | def add_watchers(obj) |
|
314 | def add_watchers(obj) | |
315 | if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) |
|
315 | if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) | |
316 | addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} |
|
316 | addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} | |
317 | unless addresses.empty? |
|
317 | unless addresses.empty? | |
318 | User.active.having_mail(addresses).each do |w| |
|
318 | User.active.having_mail(addresses).each do |w| | |
319 | obj.add_watcher(w) |
|
319 | obj.add_watcher(w) | |
320 | end |
|
320 | end | |
321 | end |
|
321 | end | |
322 | end |
|
322 | end | |
323 | end |
|
323 | end | |
324 |
|
324 | |||
325 | def get_keyword(attr, options={}) |
|
325 | def get_keyword(attr, options={}) | |
326 | @keywords ||= {} |
|
326 | @keywords ||= {} | |
327 | if @keywords.has_key?(attr) |
|
327 | if @keywords.has_key?(attr) | |
328 | @keywords[attr] |
|
328 | @keywords[attr] | |
329 | else |
|
329 | else | |
330 | @keywords[attr] = begin |
|
330 | @keywords[attr] = begin | |
331 | if (options[:override] || handler_options[:allow_override].include?(attr.to_s)) && |
|
331 | if (options[:override] || handler_options[:allow_override].include?(attr.to_s)) && | |
332 | (v = extract_keyword!(cleaned_up_text_body, attr, options[:format])) |
|
332 | (v = extract_keyword!(cleaned_up_text_body, attr, options[:format])) | |
333 | v |
|
333 | v | |
334 | elsif !handler_options[:issue][attr].blank? |
|
334 | elsif !handler_options[:issue][attr].blank? | |
335 | handler_options[:issue][attr] |
|
335 | handler_options[:issue][attr] | |
336 | end |
|
336 | end | |
337 | end |
|
337 | end | |
338 | end |
|
338 | end | |
339 | end |
|
339 | end | |
340 |
|
340 | |||
341 | # Destructively extracts the value for +attr+ in +text+ |
|
341 | # Destructively extracts the value for +attr+ in +text+ | |
342 | # Returns nil if no matching keyword found |
|
342 | # Returns nil if no matching keyword found | |
343 | def extract_keyword!(text, attr, format=nil) |
|
343 | def extract_keyword!(text, attr, format=nil) | |
344 | keys = [attr.to_s.humanize] |
|
344 | keys = [attr.to_s.humanize] | |
345 | if attr.is_a?(Symbol) |
|
345 | if attr.is_a?(Symbol) | |
346 | if user && user.language.present? |
|
346 | if user && user.language.present? | |
347 | keys << l("field_#{attr}", :default => '', :locale => user.language) |
|
347 | keys << l("field_#{attr}", :default => '', :locale => user.language) | |
348 | end |
|
348 | end | |
349 | if Setting.default_language.present? |
|
349 | if Setting.default_language.present? | |
350 | keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) |
|
350 | keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) | |
351 | end |
|
351 | end | |
352 | end |
|
352 | end | |
353 | keys.reject! {|k| k.blank?} |
|
353 | keys.reject! {|k| k.blank?} | |
354 | keys.collect! {|k| Regexp.escape(k)} |
|
354 | keys.collect! {|k| Regexp.escape(k)} | |
355 | format ||= '.+' |
|
355 | format ||= '.+' | |
356 | keyword = nil |
|
356 | keyword = nil | |
357 | regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i |
|
357 | regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i | |
358 | if m = text.match(regexp) |
|
358 | if m = text.match(regexp) | |
359 | keyword = m[2].strip |
|
359 | keyword = m[2].strip | |
360 | text.sub!(regexp, '') |
|
360 | text.sub!(regexp, '') | |
361 | end |
|
361 | end | |
362 | keyword |
|
362 | keyword | |
363 | end |
|
363 | end | |
364 |
|
364 | |||
365 | def target_project |
|
365 | def target_project | |
366 | # TODO: other ways to specify project: |
|
366 | # TODO: other ways to specify project: | |
367 | # * parse the email To field |
|
367 | # * parse the email To field | |
368 | # * specific project (eg. Setting.mail_handler_target_project) |
|
368 | # * specific project (eg. Setting.mail_handler_target_project) | |
369 | target = Project.find_by_identifier(get_keyword(:project)) |
|
369 | target = Project.find_by_identifier(get_keyword(:project)) | |
370 | if target.nil? |
|
370 | if target.nil? | |
371 | # Invalid project keyword, use the project specified as the default one |
|
371 | # Invalid project keyword, use the project specified as the default one | |
372 | default_project = handler_options[:issue][:project] |
|
372 | default_project = handler_options[:issue][:project] | |
373 | if default_project.present? |
|
373 | if default_project.present? | |
374 | target = Project.find_by_identifier(default_project) |
|
374 | target = Project.find_by_identifier(default_project) | |
375 | end |
|
375 | end | |
376 | end |
|
376 | end | |
377 | raise MissingInformation.new('Unable to determine target project') if target.nil? |
|
377 | raise MissingInformation.new('Unable to determine target project') if target.nil? | |
378 | target |
|
378 | target | |
379 | end |
|
379 | end | |
380 |
|
380 | |||
381 | # Returns a Hash of issue attributes extracted from keywords in the email body |
|
381 | # Returns a Hash of issue attributes extracted from keywords in the email body | |
382 | def issue_attributes_from_keywords(issue) |
|
382 | def issue_attributes_from_keywords(issue) | |
383 | assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue) |
|
383 | assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue) | |
384 |
|
384 | |||
385 | attrs = { |
|
385 | attrs = { | |
386 | 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id), |
|
386 | 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id), | |
387 | 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id), |
|
387 | 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id), | |
388 | 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), |
|
388 | 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id), | |
389 | 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), |
|
389 | 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), | |
390 | 'assigned_to_id' => assigned_to.try(:id), |
|
390 | 'assigned_to_id' => assigned_to.try(:id), | |
391 | 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && |
|
391 | 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && | |
392 | issue.project.shared_versions.named(k).first.try(:id), |
|
392 | issue.project.shared_versions.named(k).first.try(:id), | |
393 | 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), |
|
393 | 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), | |
394 | 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), |
|
394 | 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), | |
395 | 'estimated_hours' => get_keyword(:estimated_hours, :override => true), |
|
395 | 'estimated_hours' => get_keyword(:estimated_hours, :override => true), | |
396 | 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') |
|
396 | 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') | |
397 | }.delete_if {|k, v| v.blank? } |
|
397 | }.delete_if {|k, v| v.blank? } | |
398 |
|
398 | |||
399 | if issue.new_record? && attrs['tracker_id'].nil? |
|
399 | if issue.new_record? && attrs['tracker_id'].nil? | |
400 | attrs['tracker_id'] = issue.project.trackers.first.try(:id) |
|
400 | attrs['tracker_id'] = issue.project.trackers.first.try(:id) | |
401 | end |
|
401 | end | |
402 |
|
402 | |||
403 | attrs |
|
403 | attrs | |
404 | end |
|
404 | end | |
405 |
|
405 | |||
406 | # Returns a Hash of issue custom field values extracted from keywords in the email body |
|
406 | # Returns a Hash of issue custom field values extracted from keywords in the email body | |
407 | def custom_field_values_from_keywords(customized) |
|
407 | def custom_field_values_from_keywords(customized) | |
408 | customized.custom_field_values.inject({}) do |h, v| |
|
408 | customized.custom_field_values.inject({}) do |h, v| | |
409 | if keyword = get_keyword(v.custom_field.name, :override => true) |
|
409 | if keyword = get_keyword(v.custom_field.name, :override => true) | |
410 | h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) |
|
410 | h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized) | |
411 | end |
|
411 | end | |
412 | h |
|
412 | h | |
413 | end |
|
413 | end | |
414 | end |
|
414 | end | |
415 |
|
415 | |||
416 | # Returns the text/plain part of the email |
|
416 | # Returns the text/plain part of the email | |
417 | # If not found (eg. HTML-only email), returns the body with tags removed |
|
417 | # If not found (eg. HTML-only email), returns the body with tags removed | |
418 | def plain_text_body |
|
418 | def plain_text_body | |
419 | return @plain_text_body unless @plain_text_body.nil? |
|
419 | return @plain_text_body unless @plain_text_body.nil? | |
420 |
|
420 | |||
421 | parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present? |
|
421 | parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present? | |
422 | text_parts |
|
422 | text_parts | |
423 | elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present? |
|
423 | elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present? | |
424 | html_parts |
|
424 | html_parts | |
425 | else |
|
425 | else | |
426 | [email] |
|
426 | [email] | |
427 | end |
|
427 | end | |
428 |
|
428 | |||
429 | parts.reject! do |part| |
|
429 | parts.reject! do |part| | |
430 | part.attachment? |
|
430 | part.attachment? | |
431 | end |
|
431 | end | |
432 |
|
432 | |||
433 | @plain_text_body = parts.map do |p| |
|
433 | @plain_text_body = parts.map do |p| | |
434 | body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ? |
|
434 | body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ? | |
435 | Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset |
|
435 | Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset | |
436 | Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset) |
|
|||
437 | end.join("\r\n") |
|
|||
438 |
|
436 | |||
439 | # strip html tags and remove doctype directive |
|
437 | body = Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset) | |
440 | if parts.any? {|p| p.mime_type == 'text/html'} |
|
438 | # convert html parts to text | |
441 | @plain_text_body = strip_tags(@plain_text_body.strip) |
|
439 | p.mime_type == 'text/html' ? self.class.html_body_to_text(body) : body | |
442 | @plain_text_body.sub! %r{^<!DOCTYPE .*$}, '' |
|
440 | end.join("\r\n") | |
443 | end |
|
|||
444 |
|
441 | |||
445 | @plain_text_body |
|
442 | @plain_text_body | |
446 | end |
|
443 | end | |
447 |
|
444 | |||
448 | def cleaned_up_text_body |
|
445 | def cleaned_up_text_body | |
449 | @cleaned_up_text_body ||= cleanup_body(plain_text_body) |
|
446 | @cleaned_up_text_body ||= cleanup_body(plain_text_body) | |
450 | end |
|
447 | end | |
451 |
|
448 | |||
452 | def cleaned_up_subject |
|
449 | def cleaned_up_subject | |
453 | subject = email.subject.to_s |
|
450 | subject = email.subject.to_s | |
454 | subject.strip[0,255] |
|
451 | subject.strip[0,255] | |
455 | end |
|
452 | end | |
456 |
|
453 | |||
|
454 | # Converts a HTML email body to text | |||
|
455 | def self.html_body_to_text(html) | |||
|
456 | Redmine::WikiFormatting.html_parser.to_text(html) | |||
|
457 | end | |||
|
458 | ||||
457 | def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil) |
|
459 | def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil) | |
458 | limit ||= object.class.columns_hash[attribute.to_s].limit || 255 |
|
460 | limit ||= object.class.columns_hash[attribute.to_s].limit || 255 | |
459 | value = value.to_s.slice(0, limit) |
|
461 | value = value.to_s.slice(0, limit) | |
460 | object.send("#{attribute}=", value) |
|
462 | object.send("#{attribute}=", value) | |
461 | end |
|
463 | end | |
462 |
|
464 | |||
463 | # Returns a User from an email address and a full name |
|
465 | # Returns a User from an email address and a full name | |
464 | def self.new_user_from_attributes(email_address, fullname=nil) |
|
466 | def self.new_user_from_attributes(email_address, fullname=nil) | |
465 | user = User.new |
|
467 | user = User.new | |
466 |
|
468 | |||
467 | # Truncating the email address would result in an invalid format |
|
469 | # Truncating the email address would result in an invalid format | |
468 | user.mail = email_address |
|
470 | user.mail = email_address | |
469 | assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT) |
|
471 | assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT) | |
470 |
|
472 | |||
471 | names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split |
|
473 | names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split | |
472 | assign_string_attribute_with_limit(user, 'firstname', names.shift, 30) |
|
474 | assign_string_attribute_with_limit(user, 'firstname', names.shift, 30) | |
473 | assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30) |
|
475 | assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30) | |
474 | user.lastname = '-' if user.lastname.blank? |
|
476 | user.lastname = '-' if user.lastname.blank? | |
475 | user.language = Setting.default_language |
|
477 | user.language = Setting.default_language | |
476 | user.generate_password = true |
|
478 | user.generate_password = true | |
477 | user.mail_notification = 'only_my_events' |
|
479 | user.mail_notification = 'only_my_events' | |
478 |
|
480 | |||
479 | unless user.valid? |
|
481 | unless user.valid? | |
480 | user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank? |
|
482 | user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank? | |
481 | user.firstname = "-" unless user.errors[:firstname].blank? |
|
483 | user.firstname = "-" unless user.errors[:firstname].blank? | |
482 | (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank? |
|
484 | (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank? | |
483 | end |
|
485 | end | |
484 |
|
486 | |||
485 | user |
|
487 | user | |
486 | end |
|
488 | end | |
487 |
|
489 | |||
488 | # Creates a User for the +email+ sender |
|
490 | # Creates a User for the +email+ sender | |
489 | # Returns the user or nil if it could not be created |
|
491 | # Returns the user or nil if it could not be created | |
490 | def create_user_from_email |
|
492 | def create_user_from_email | |
491 | from = email.header['from'].to_s |
|
493 | from = email.header['from'].to_s | |
492 | addr, name = from, nil |
|
494 | addr, name = from, nil | |
493 | if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/) |
|
495 | if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/) | |
494 | addr, name = m[2], m[1] |
|
496 | addr, name = m[2], m[1] | |
495 | end |
|
497 | end | |
496 | if addr.present? |
|
498 | if addr.present? | |
497 | user = self.class.new_user_from_attributes(addr, name) |
|
499 | user = self.class.new_user_from_attributes(addr, name) | |
498 | if handler_options[:no_notification] |
|
500 | if handler_options[:no_notification] | |
499 | user.mail_notification = 'none' |
|
501 | user.mail_notification = 'none' | |
500 | end |
|
502 | end | |
501 | if user.save |
|
503 | if user.save | |
502 | user |
|
504 | user | |
503 | else |
|
505 | else | |
504 | logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger |
|
506 | logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger | |
505 | nil |
|
507 | nil | |
506 | end |
|
508 | end | |
507 | else |
|
509 | else | |
508 | logger.error "MailHandler: failed to create User: no FROM address found" if logger |
|
510 | logger.error "MailHandler: failed to create User: no FROM address found" if logger | |
509 | nil |
|
511 | nil | |
510 | end |
|
512 | end | |
511 | end |
|
513 | end | |
512 |
|
514 | |||
513 | # Adds the newly created user to default group |
|
515 | # Adds the newly created user to default group | |
514 | def add_user_to_group(default_group) |
|
516 | def add_user_to_group(default_group) | |
515 | if default_group.present? |
|
517 | if default_group.present? | |
516 | default_group.split(',').each do |group_name| |
|
518 | default_group.split(',').each do |group_name| | |
517 | if group = Group.named(group_name).first |
|
519 | if group = Group.named(group_name).first | |
518 | group.users << @user |
|
520 | group.users << @user | |
519 | elsif logger |
|
521 | elsif logger | |
520 | logger.warn "MailHandler: could not add user to [#{group_name}], group not found" |
|
522 | logger.warn "MailHandler: could not add user to [#{group_name}], group not found" | |
521 | end |
|
523 | end | |
522 | end |
|
524 | end | |
523 | end |
|
525 | end | |
524 | end |
|
526 | end | |
525 |
|
527 | |||
526 | # Removes the email body of text after the truncation configurations. |
|
528 | # Removes the email body of text after the truncation configurations. | |
527 | def cleanup_body(body) |
|
529 | def cleanup_body(body) | |
528 | delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} |
|
530 | delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)} | |
529 | unless delimiters.empty? |
|
531 | unless delimiters.empty? | |
530 | regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) |
|
532 | regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE) | |
531 | body = body.gsub(regex, '') |
|
533 | body = body.gsub(regex, '') | |
532 | end |
|
534 | end | |
533 | body.strip |
|
535 | body.strip | |
534 | end |
|
536 | end | |
535 |
|
537 | |||
536 | def find_assignee_from_keyword(keyword, issue) |
|
538 | def find_assignee_from_keyword(keyword, issue) | |
537 | keyword = keyword.to_s.downcase |
|
539 | keyword = keyword.to_s.downcase | |
538 | assignable = issue.assignable_users |
|
540 | assignable = issue.assignable_users | |
539 | assignee = nil |
|
541 | assignee = nil | |
540 | assignee ||= assignable.detect {|a| |
|
542 | assignee ||= assignable.detect {|a| | |
541 | a.mail.to_s.downcase == keyword || |
|
543 | a.mail.to_s.downcase == keyword || | |
542 | a.login.to_s.downcase == keyword |
|
544 | a.login.to_s.downcase == keyword | |
543 | } |
|
545 | } | |
544 | if assignee.nil? && keyword.match(/ /) |
|
546 | if assignee.nil? && keyword.match(/ /) | |
545 | firstname, lastname = *(keyword.split) # "First Last Throwaway" |
|
547 | firstname, lastname = *(keyword.split) # "First Last Throwaway" | |
546 | assignee ||= assignable.detect {|a| |
|
548 | assignee ||= assignable.detect {|a| | |
547 | a.is_a?(User) && a.firstname.to_s.downcase == firstname && |
|
549 | a.is_a?(User) && a.firstname.to_s.downcase == firstname && | |
548 | a.lastname.to_s.downcase == lastname |
|
550 | a.lastname.to_s.downcase == lastname | |
549 | } |
|
551 | } | |
550 | end |
|
552 | end | |
551 | if assignee.nil? |
|
553 | if assignee.nil? | |
552 | assignee ||= assignable.detect {|a| a.name.downcase == keyword} |
|
554 | assignee ||= assignable.detect {|a| a.name.downcase == keyword} | |
553 | end |
|
555 | end | |
554 | assignee |
|
556 | assignee | |
555 | end |
|
557 | end | |
556 | end |
|
558 | end |
@@ -1,277 +1,274 | |||||
1 | # Redmine - project management software |
|
1 | # Redmine - project management software | |
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
14 | # You should have received a copy of the GNU General Public License | |
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 'redmine/core_ext' |
|
18 | require 'redmine/core_ext' | |
19 |
|
19 | |||
20 | begin |
|
20 | begin | |
21 | require 'rmagick' unless Object.const_defined?(:Magick) |
|
21 | require 'rmagick' unless Object.const_defined?(:Magick) | |
22 | rescue LoadError |
|
22 | rescue LoadError | |
23 | # RMagick is not available |
|
23 | # RMagick is not available | |
24 | end |
|
24 | end | |
25 | begin |
|
25 | begin | |
26 | require 'redcarpet' unless Object.const_defined?(:Redcarpet) |
|
26 | require 'redcarpet' unless Object.const_defined?(:Redcarpet) | |
27 | rescue LoadError |
|
27 | rescue LoadError | |
28 | # Redcarpet is not available |
|
28 | # Redcarpet is not available | |
29 | end |
|
29 | end | |
30 |
|
30 | |||
31 | require 'redmine/scm/base' |
|
31 | require 'redmine/scm/base' | |
32 | require 'redmine/access_control' |
|
32 | require 'redmine/access_control' | |
33 | require 'redmine/access_keys' |
|
33 | require 'redmine/access_keys' | |
34 | require 'redmine/activity' |
|
34 | require 'redmine/activity' | |
35 | require 'redmine/activity/fetcher' |
|
35 | require 'redmine/activity/fetcher' | |
36 | require 'redmine/ciphering' |
|
36 | require 'redmine/ciphering' | |
37 | require 'redmine/codeset_util' |
|
37 | require 'redmine/codeset_util' | |
38 | require 'redmine/field_format' |
|
38 | require 'redmine/field_format' | |
39 | require 'redmine/i18n' |
|
39 | require 'redmine/i18n' | |
40 | require 'redmine/menu_manager' |
|
40 | require 'redmine/menu_manager' | |
41 | require 'redmine/notifiable' |
|
41 | require 'redmine/notifiable' | |
42 | require 'redmine/platform' |
|
42 | require 'redmine/platform' | |
43 | require 'redmine/mime_type' |
|
43 | require 'redmine/mime_type' | |
44 | require 'redmine/notifiable' |
|
44 | require 'redmine/notifiable' | |
45 | require 'redmine/search' |
|
45 | require 'redmine/search' | |
46 | require 'redmine/syntax_highlighting' |
|
46 | require 'redmine/syntax_highlighting' | |
47 | require 'redmine/thumbnail' |
|
47 | require 'redmine/thumbnail' | |
48 | require 'redmine/unified_diff' |
|
48 | require 'redmine/unified_diff' | |
49 | require 'redmine/utils' |
|
49 | require 'redmine/utils' | |
50 | require 'redmine/version' |
|
50 | require 'redmine/version' | |
51 | require 'redmine/wiki_formatting' |
|
51 | require 'redmine/wiki_formatting' | |
52 |
|
52 | |||
53 | require 'redmine/default_data/loader' |
|
53 | require 'redmine/default_data/loader' | |
54 | require 'redmine/helpers/calendar' |
|
54 | require 'redmine/helpers/calendar' | |
55 | require 'redmine/helpers/diff' |
|
55 | require 'redmine/helpers/diff' | |
56 | require 'redmine/helpers/gantt' |
|
56 | require 'redmine/helpers/gantt' | |
57 | require 'redmine/helpers/time_report' |
|
57 | require 'redmine/helpers/time_report' | |
58 | require 'redmine/views/other_formats_builder' |
|
58 | require 'redmine/views/other_formats_builder' | |
59 | require 'redmine/views/labelled_form_builder' |
|
59 | require 'redmine/views/labelled_form_builder' | |
60 | require 'redmine/views/builders' |
|
60 | require 'redmine/views/builders' | |
61 |
|
61 | |||
62 | require 'redmine/themes' |
|
62 | require 'redmine/themes' | |
63 | require 'redmine/hook' |
|
63 | require 'redmine/hook' | |
64 | require 'redmine/plugin' |
|
64 | require 'redmine/plugin' | |
65 |
|
65 | |||
66 | Redmine::Scm::Base.add "Subversion" |
|
66 | Redmine::Scm::Base.add "Subversion" | |
67 | Redmine::Scm::Base.add "Darcs" |
|
67 | Redmine::Scm::Base.add "Darcs" | |
68 | Redmine::Scm::Base.add "Mercurial" |
|
68 | Redmine::Scm::Base.add "Mercurial" | |
69 | Redmine::Scm::Base.add "Cvs" |
|
69 | Redmine::Scm::Base.add "Cvs" | |
70 | Redmine::Scm::Base.add "Bazaar" |
|
70 | Redmine::Scm::Base.add "Bazaar" | |
71 | Redmine::Scm::Base.add "Git" |
|
71 | Redmine::Scm::Base.add "Git" | |
72 | Redmine::Scm::Base.add "Filesystem" |
|
72 | Redmine::Scm::Base.add "Filesystem" | |
73 |
|
73 | |||
74 | # Permissions |
|
74 | # Permissions | |
75 | Redmine::AccessControl.map do |map| |
|
75 | Redmine::AccessControl.map do |map| | |
76 | map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true |
|
76 | map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true | |
77 | map.permission :search_project, {:search => :index}, :public => true, :read => true |
|
77 | map.permission :search_project, {:search => :index}, :public => true, :read => true | |
78 | map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin |
|
78 | map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin | |
79 | map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member |
|
79 | map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member | |
80 | map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true |
|
80 | map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true | |
81 | map.permission :select_project_modules, {:projects => :modules}, :require => :member |
|
81 | map.permission :select_project_modules, {:projects => :modules}, :require => :member | |
82 | map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true |
|
82 | map.permission :view_members, {:members => [:index, :show]}, :public => true, :read => true | |
83 | map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member |
|
83 | map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :new, :create, :update, :destroy, :autocomplete]}, :require => :member | |
84 | map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member |
|
84 | map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member | |
85 | map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member |
|
85 | map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member | |
86 |
|
86 | |||
87 | map.project_module :issue_tracking do |map| |
|
87 | map.project_module :issue_tracking do |map| | |
88 | # Issue categories |
|
88 | # Issue categories | |
89 | map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member |
|
89 | map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member | |
90 | # Issues |
|
90 | # Issues | |
91 | map.permission :view_issues, {:issues => [:index, :show], |
|
91 | map.permission :view_issues, {:issues => [:index, :show], | |
92 | :auto_complete => [:issues], |
|
92 | :auto_complete => [:issues], | |
93 | :context_menus => [:issues], |
|
93 | :context_menus => [:issues], | |
94 | :versions => [:index, :show, :status_by], |
|
94 | :versions => [:index, :show, :status_by], | |
95 | :journals => [:index, :diff], |
|
95 | :journals => [:index, :diff], | |
96 | :queries => :index, |
|
96 | :queries => :index, | |
97 | :reports => [:issue_report, :issue_report_details]}, |
|
97 | :reports => [:issue_report, :issue_report_details]}, | |
98 | :read => true |
|
98 | :read => true | |
99 | map.permission :add_issues, {:issues => [:new, :create], :attachments => :upload} |
|
99 | map.permission :add_issues, {:issues => [:new, :create], :attachments => :upload} | |
100 | map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update], :journals => [:new], :attachments => :upload} |
|
100 | map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update], :journals => [:new], :attachments => :upload} | |
101 | map.permission :copy_issues, {:issues => [:new, :create, :bulk_edit, :bulk_update], :attachments => :upload} |
|
101 | map.permission :copy_issues, {:issues => [:new, :create, :bulk_edit, :bulk_update], :attachments => :upload} | |
102 | map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]} |
|
102 | map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]} | |
103 | map.permission :manage_subtasks, {} |
|
103 | map.permission :manage_subtasks, {} | |
104 | map.permission :set_issues_private, {} |
|
104 | map.permission :set_issues_private, {} | |
105 | map.permission :set_own_issues_private, {}, :require => :loggedin |
|
105 | map.permission :set_own_issues_private, {}, :require => :loggedin | |
106 | map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload} |
|
106 | map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload} | |
107 | map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin |
|
107 | map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin | |
108 | map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin |
|
108 | map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin | |
109 | map.permission :view_private_notes, {}, :read => true, :require => :member |
|
109 | map.permission :view_private_notes, {}, :read => true, :require => :member | |
110 | map.permission :set_notes_private, {}, :require => :member |
|
110 | map.permission :set_notes_private, {}, :require => :member | |
111 | map.permission :delete_issues, {:issues => :destroy}, :require => :member |
|
111 | map.permission :delete_issues, {:issues => :destroy}, :require => :member | |
112 | # Queries |
|
112 | # Queries | |
113 | map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member |
|
113 | map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member | |
114 | map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin |
|
114 | map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin | |
115 | # Watchers |
|
115 | # Watchers | |
116 | map.permission :view_issue_watchers, {}, :read => true |
|
116 | map.permission :view_issue_watchers, {}, :read => true | |
117 | map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]} |
|
117 | map.permission :add_issue_watchers, {:watchers => [:new, :create, :append, :autocomplete_for_user]} | |
118 | map.permission :delete_issue_watchers, {:watchers => :destroy} |
|
118 | map.permission :delete_issue_watchers, {:watchers => :destroy} | |
119 | end |
|
119 | end | |
120 |
|
120 | |||
121 | map.project_module :time_tracking do |map| |
|
121 | map.project_module :time_tracking do |map| | |
122 | map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin |
|
122 | map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin | |
123 | map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true |
|
123 | map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true | |
124 | map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member |
|
124 | map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member | |
125 | map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin |
|
125 | map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin | |
126 | map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member |
|
126 | map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member | |
127 | end |
|
127 | end | |
128 |
|
128 | |||
129 | map.project_module :news do |map| |
|
129 | map.project_module :news do |map| | |
130 | map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member |
|
130 | map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy], :attachments => :upload}, :require => :member | |
131 | map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true |
|
131 | map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true | |
132 | map.permission :comment_news, {:comments => :create} |
|
132 | map.permission :comment_news, {:comments => :create} | |
133 | end |
|
133 | end | |
134 |
|
134 | |||
135 | map.project_module :documents do |map| |
|
135 | map.project_module :documents do |map| | |
136 | map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin |
|
136 | map.permission :add_documents, {:documents => [:new, :create, :add_attachment], :attachments => :upload}, :require => :loggedin | |
137 | map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin |
|
137 | map.permission :edit_documents, {:documents => [:edit, :update, :add_attachment], :attachments => :upload}, :require => :loggedin | |
138 | map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin |
|
138 | map.permission :delete_documents, {:documents => [:destroy]}, :require => :loggedin | |
139 | map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true |
|
139 | map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true | |
140 | end |
|
140 | end | |
141 |
|
141 | |||
142 | map.project_module :files do |map| |
|
142 | map.project_module :files do |map| | |
143 | map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin |
|
143 | map.permission :manage_files, {:files => [:new, :create], :attachments => :upload}, :require => :loggedin | |
144 | map.permission :view_files, {:files => :index, :versions => :download}, :read => true |
|
144 | map.permission :view_files, {:files => :index, :versions => :download}, :read => true | |
145 | end |
|
145 | end | |
146 |
|
146 | |||
147 | map.project_module :wiki do |map| |
|
147 | map.project_module :wiki do |map| | |
148 | map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member |
|
148 | map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member | |
149 | map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member |
|
149 | map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member | |
150 | map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member |
|
150 | map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member | |
151 | map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true |
|
151 | map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true | |
152 | map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true |
|
152 | map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true | |
153 | map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true |
|
153 | map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true | |
154 | map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment], :attachments => :upload |
|
154 | map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment], :attachments => :upload | |
155 | map.permission :delete_wiki_pages_attachments, {} |
|
155 | map.permission :delete_wiki_pages_attachments, {} | |
156 | map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member |
|
156 | map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member | |
157 | end |
|
157 | end | |
158 |
|
158 | |||
159 | map.project_module :repository do |map| |
|
159 | map.project_module :repository do |map| | |
160 | map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member |
|
160 | map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member | |
161 | map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true |
|
161 | map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true | |
162 | map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true |
|
162 | map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true | |
163 | map.permission :commit_access, {} |
|
163 | map.permission :commit_access, {} | |
164 | map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]} |
|
164 | map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]} | |
165 | end |
|
165 | end | |
166 |
|
166 | |||
167 | map.project_module :boards do |map| |
|
167 | map.project_module :boards do |map| | |
168 | map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member |
|
168 | map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member | |
169 | map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true |
|
169 | map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true | |
170 | map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload} |
|
170 | map.permission :add_messages, {:messages => [:new, :reply, :quote], :attachments => :upload} | |
171 | map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member |
|
171 | map.permission :edit_messages, {:messages => :edit, :attachments => :upload}, :require => :member | |
172 | map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin |
|
172 | map.permission :edit_own_messages, {:messages => :edit, :attachments => :upload}, :require => :loggedin | |
173 | map.permission :delete_messages, {:messages => :destroy}, :require => :member |
|
173 | map.permission :delete_messages, {:messages => :destroy}, :require => :member | |
174 | map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin |
|
174 | map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin | |
175 | end |
|
175 | end | |
176 |
|
176 | |||
177 | map.project_module :calendar do |map| |
|
177 | map.project_module :calendar do |map| | |
178 | map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true |
|
178 | map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true | |
179 | end |
|
179 | end | |
180 |
|
180 | |||
181 | map.project_module :gantt do |map| |
|
181 | map.project_module :gantt do |map| | |
182 | map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true |
|
182 | map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true | |
183 | end |
|
183 | end | |
184 | end |
|
184 | end | |
185 |
|
185 | |||
186 | Redmine::MenuManager.map :top_menu do |menu| |
|
186 | Redmine::MenuManager.map :top_menu do |menu| | |
187 | menu.push :home, :home_path |
|
187 | menu.push :home, :home_path | |
188 | menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? } |
|
188 | menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? } | |
189 | menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural |
|
189 | menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural | |
190 | menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true |
|
190 | menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true | |
191 | menu.push :help, Redmine::Info.help_url, :last => true |
|
191 | menu.push :help, Redmine::Info.help_url, :last => true | |
192 | end |
|
192 | end | |
193 |
|
193 | |||
194 | Redmine::MenuManager.map :account_menu do |menu| |
|
194 | Redmine::MenuManager.map :account_menu do |menu| | |
195 | menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? } |
|
195 | menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? } | |
196 | menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? } |
|
196 | menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? } | |
197 | menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? } |
|
197 | menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? } | |
198 | menu.push :logout, :signout_path, :html => {:method => 'post'}, :if => Proc.new { User.current.logged? } |
|
198 | menu.push :logout, :signout_path, :html => {:method => 'post'}, :if => Proc.new { User.current.logged? } | |
199 | end |
|
199 | end | |
200 |
|
200 | |||
201 | Redmine::MenuManager.map :application_menu do |menu| |
|
201 | Redmine::MenuManager.map :application_menu do |menu| | |
202 | # Empty |
|
202 | # Empty | |
203 | end |
|
203 | end | |
204 |
|
204 | |||
205 | Redmine::MenuManager.map :admin_menu do |menu| |
|
205 | Redmine::MenuManager.map :admin_menu do |menu| | |
206 | menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural |
|
206 | menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural | |
207 | menu.push :users, {:controller => 'users'}, :caption => :label_user_plural |
|
207 | menu.push :users, {:controller => 'users'}, :caption => :label_user_plural | |
208 | menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural |
|
208 | menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural | |
209 | menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions |
|
209 | menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions | |
210 | menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural |
|
210 | menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural | |
211 | menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural, |
|
211 | menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural, | |
212 | :html => {:class => 'issue_statuses'} |
|
212 | :html => {:class => 'issue_statuses'} | |
213 | menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow |
|
213 | menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow | |
214 | menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural, |
|
214 | menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural, | |
215 | :html => {:class => 'custom_fields'} |
|
215 | :html => {:class => 'custom_fields'} | |
216 | menu.push :enumerations, {:controller => 'enumerations'} |
|
216 | menu.push :enumerations, {:controller => 'enumerations'} | |
217 | menu.push :settings, {:controller => 'settings'} |
|
217 | menu.push :settings, {:controller => 'settings'} | |
218 | menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'}, |
|
218 | menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'}, | |
219 | :html => {:class => 'server_authentication'} |
|
219 | :html => {:class => 'server_authentication'} | |
220 | menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true |
|
220 | menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true | |
221 | menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true |
|
221 | menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true | |
222 | end |
|
222 | end | |
223 |
|
223 | |||
224 | Redmine::MenuManager.map :project_menu do |menu| |
|
224 | Redmine::MenuManager.map :project_menu do |menu| | |
225 | menu.push :overview, { :controller => 'projects', :action => 'show' } |
|
225 | menu.push :overview, { :controller => 'projects', :action => 'show' } | |
226 | menu.push :activity, { :controller => 'activities', :action => 'index' } |
|
226 | menu.push :activity, { :controller => 'activities', :action => 'index' } | |
227 | menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id, |
|
227 | menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id, | |
228 | :if => Proc.new { |p| p.shared_versions.any? } |
|
228 | :if => Proc.new { |p| p.shared_versions.any? } | |
229 | menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural |
|
229 | menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural | |
230 | menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new, |
|
230 | menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new, | |
231 | :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }, |
|
231 | :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }, | |
232 | :if => Proc.new { |p| p.trackers.any? }, |
|
232 | :if => Proc.new { |p| p.trackers.any? }, | |
233 | :permission => :add_issues |
|
233 | :permission => :add_issues | |
234 | menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt |
|
234 | menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt | |
235 | menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar |
|
235 | menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar | |
236 | menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural |
|
236 | menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural | |
237 | menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural |
|
237 | menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural | |
238 | menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id, |
|
238 | menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id, | |
239 | :if => Proc.new { |p| p.wiki && !p.wiki.new_record? } |
|
239 | :if => Proc.new { |p| p.wiki && !p.wiki.new_record? } | |
240 | menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, |
|
240 | menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id, | |
241 | :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural |
|
241 | :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural | |
242 | menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id |
|
242 | menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id | |
243 | menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil }, |
|
243 | menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil }, | |
244 | :if => Proc.new { |p| p.repository && !p.repository.new_record? } |
|
244 | :if => Proc.new { |p| p.repository && !p.repository.new_record? } | |
245 | menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true |
|
245 | menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true | |
246 | end |
|
246 | end | |
247 |
|
247 | |||
248 | Redmine::Activity.map do |activity| |
|
248 | Redmine::Activity.map do |activity| | |
249 | activity.register :issues, :class_name => %w(Issue Journal) |
|
249 | activity.register :issues, :class_name => %w(Issue Journal) | |
250 | activity.register :changesets |
|
250 | activity.register :changesets | |
251 | activity.register :news |
|
251 | activity.register :news | |
252 | activity.register :documents, :class_name => %w(Document Attachment) |
|
252 | activity.register :documents, :class_name => %w(Document Attachment) | |
253 | activity.register :files, :class_name => 'Attachment' |
|
253 | activity.register :files, :class_name => 'Attachment' | |
254 | activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false |
|
254 | activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false | |
255 | activity.register :messages, :default => false |
|
255 | activity.register :messages, :default => false | |
256 | activity.register :time_entries, :default => false |
|
256 | activity.register :time_entries, :default => false | |
257 | end |
|
257 | end | |
258 |
|
258 | |||
259 | Redmine::Search.map do |search| |
|
259 | Redmine::Search.map do |search| | |
260 | search.register :issues |
|
260 | search.register :issues | |
261 | search.register :news |
|
261 | search.register :news | |
262 | search.register :documents |
|
262 | search.register :documents | |
263 | search.register :changesets |
|
263 | search.register :changesets | |
264 | search.register :wiki_pages |
|
264 | search.register :wiki_pages | |
265 | search.register :messages |
|
265 | search.register :messages | |
266 | search.register :projects |
|
266 | search.register :projects | |
267 | end |
|
267 | end | |
268 |
|
268 | |||
269 | Redmine::WikiFormatting.map do |format| |
|
269 | Redmine::WikiFormatting.map do |format| | |
270 | format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper |
|
270 | format.register :textile | |
271 | if Object.const_defined?(:Redcarpet) |
|
271 | format.register :markdown if Object.const_defined?(:Redcarpet) | |
272 | format.register :markdown, Redmine::WikiFormatting::Markdown::Formatter, Redmine::WikiFormatting::Markdown::Helper, |
|
|||
273 | :label => 'Markdown' |
|
|||
274 | end |
|
|||
275 | end |
|
272 | end | |
276 |
|
273 | |||
277 | ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler |
|
274 | ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler |
@@ -1,182 +1,198 | |||||
1 | # Redmine - project management software |
|
1 | # Redmine - project management software | |
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
2 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |
3 | # |
|
3 | # | |
4 | # This program is free software; you can redistribute it and/or |
|
4 | # This program is free software; you can redistribute it and/or | |
5 | # modify it under the terms of the GNU General Public License |
|
5 | # modify it under the terms of the GNU General Public License | |
6 | # as published by the Free Software Foundation; either version 2 |
|
6 | # as published by the Free Software Foundation; either version 2 | |
7 | # of the License, or (at your option) any later version. |
|
7 | # of the License, or (at your option) any later version. | |
8 | # |
|
8 | # | |
9 | # This program is distributed in the hope that it will be useful, |
|
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. |
|
12 | # GNU General Public License for more details. | |
13 | # |
|
13 | # | |
14 | # You should have received a copy of the GNU General Public License |
|
14 | # You should have received a copy of the GNU General Public License | |
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 'digest/md5' |
|
18 | require 'digest/md5' | |
19 |
|
19 | |||
20 | module Redmine |
|
20 | module Redmine | |
21 | module WikiFormatting |
|
21 | module WikiFormatting | |
22 | class StaleSectionError < Exception; end |
|
22 | class StaleSectionError < Exception; end | |
23 |
|
23 | |||
24 | @@formatters = {} |
|
24 | @@formatters = {} | |
25 |
|
25 | |||
26 | class << self |
|
26 | class << self | |
27 | def map |
|
27 | def map | |
28 | yield self |
|
28 | yield self | |
29 | end |
|
29 | end | |
30 |
|
30 | |||
31 |
def register(name, |
|
31 | def register(name, *args) | |
|
32 | options = args.last.is_a?(Hash) ? args.pop : {} | |||
32 | name = name.to_s |
|
33 | name = name.to_s | |
33 | raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name] |
|
34 | raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name] | |
|
35 | ||||
|
36 | formatter, helper, parser = args.any? ? | |||
|
37 | args : | |||
|
38 | %w(Formatter Helper HtmlParser).map {|m| "Redmine::WikiFormatting::#{name.classify}::#{m}".constantize} | |||
|
39 | ||||
34 | @@formatters[name] = { |
|
40 | @@formatters[name] = { | |
35 | :formatter => formatter, |
|
41 | :formatter => formatter, | |
36 | :helper => helper, |
|
42 | :helper => helper, | |
|
43 | :html_parser => parser, | |||
37 | :label => options[:label] || name.humanize |
|
44 | :label => options[:label] || name.humanize | |
38 | } |
|
45 | } | |
39 | end |
|
46 | end | |
40 |
|
47 | |||
41 | def formatter |
|
48 | def formatter | |
42 | formatter_for(Setting.text_formatting) |
|
49 | formatter_for(Setting.text_formatting) | |
43 | end |
|
50 | end | |
44 |
|
51 | |||
|
52 | def html_parser | |||
|
53 | html_parser_for(Setting.text_formatting) | |||
|
54 | end | |||
|
55 | ||||
45 | def formatter_for(name) |
|
56 | def formatter_for(name) | |
46 | entry = @@formatters[name.to_s] |
|
57 | entry = @@formatters[name.to_s] | |
47 | (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter |
|
58 | (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter | |
48 | end |
|
59 | end | |
49 |
|
60 | |||
50 | def helper_for(name) |
|
61 | def helper_for(name) | |
51 | entry = @@formatters[name.to_s] |
|
62 | entry = @@formatters[name.to_s] | |
52 | (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper |
|
63 | (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper | |
53 | end |
|
64 | end | |
54 |
|
65 | |||
|
66 | def html_parser_for(name) | |||
|
67 | entry = @@formatters[name.to_s] | |||
|
68 | (entry && entry[:html_parser]) || Redmine::WikiFormatting::HtmlParser | |||
|
69 | end | |||
|
70 | ||||
55 | def format_names |
|
71 | def format_names | |
56 | @@formatters.keys.map |
|
72 | @@formatters.keys.map | |
57 | end |
|
73 | end | |
58 |
|
74 | |||
59 | def formats_for_select |
|
75 | def formats_for_select | |
60 | @@formatters.map {|name, options| [options[:label], name]} |
|
76 | @@formatters.map {|name, options| [options[:label], name]} | |
61 | end |
|
77 | end | |
62 |
|
78 | |||
63 | def to_html(format, text, options = {}) |
|
79 | def to_html(format, text, options = {}) | |
64 | text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute]) |
|
80 | text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, text, options[:object], options[:attribute]) | |
65 | # Text retrieved from the cache store may be frozen |
|
81 | # Text retrieved from the cache store may be frozen | |
66 | # We need to dup it so we can do in-place substitutions with gsub! |
|
82 | # We need to dup it so we can do in-place substitutions with gsub! | |
67 | cache_store.fetch cache_key do |
|
83 | cache_store.fetch cache_key do | |
68 | formatter_for(format).new(text).to_html |
|
84 | formatter_for(format).new(text).to_html | |
69 | end.dup |
|
85 | end.dup | |
70 | else |
|
86 | else | |
71 | formatter_for(format).new(text).to_html |
|
87 | formatter_for(format).new(text).to_html | |
72 | end |
|
88 | end | |
73 | text |
|
89 | text | |
74 | end |
|
90 | end | |
75 |
|
91 | |||
76 | # Returns true if the text formatter supports single section edit |
|
92 | # Returns true if the text formatter supports single section edit | |
77 | def supports_section_edit? |
|
93 | def supports_section_edit? | |
78 | (formatter.instance_methods & ['update_section', :update_section]).any? |
|
94 | (formatter.instance_methods & ['update_section', :update_section]).any? | |
79 | end |
|
95 | end | |
80 |
|
96 | |||
81 | # Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done |
|
97 | # Returns a cache key for the given text +format+, +text+, +object+ and +attribute+ or nil if no caching should be done | |
82 | def cache_key_for(format, text, object, attribute) |
|
98 | def cache_key_for(format, text, object, attribute) | |
83 | if object && attribute && !object.new_record? && format.present? |
|
99 | if object && attribute && !object.new_record? && format.present? | |
84 | "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}" |
|
100 | "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{Digest::MD5.hexdigest text}" | |
85 | end |
|
101 | end | |
86 | end |
|
102 | end | |
87 |
|
103 | |||
88 | # Returns the cache store used to cache HTML output |
|
104 | # Returns the cache store used to cache HTML output | |
89 | def cache_store |
|
105 | def cache_store | |
90 | ActionController::Base.cache_store |
|
106 | ActionController::Base.cache_store | |
91 | end |
|
107 | end | |
92 | end |
|
108 | end | |
93 |
|
109 | |||
94 | module LinksHelper |
|
110 | module LinksHelper | |
95 | AUTO_LINK_RE = %r{ |
|
111 | AUTO_LINK_RE = %r{ | |
96 | ( # leading text |
|
112 | ( # leading text | |
97 | <\w+[^>]*?>| # leading HTML tag, or |
|
113 | <\w+[^>]*?>| # leading HTML tag, or | |
98 | [\s\(\[,;]| # leading punctuation, or |
|
114 | [\s\(\[,;]| # leading punctuation, or | |
99 | ^ # beginning of line |
|
115 | ^ # beginning of line | |
100 | ) |
|
116 | ) | |
101 | ( |
|
117 | ( | |
102 | (?:https?://)| # protocol spec, or |
|
118 | (?:https?://)| # protocol spec, or | |
103 | (?:s?ftps?://)| |
|
119 | (?:s?ftps?://)| | |
104 | (?:www\.) # www.* |
|
120 | (?:www\.) # www.* | |
105 | ) |
|
121 | ) | |
106 | ( |
|
122 | ( | |
107 | ([^<]\S*?) # url |
|
123 | ([^<]\S*?) # url | |
108 | (\/)? # slash |
|
124 | (\/)? # slash | |
109 | ) |
|
125 | ) | |
110 | ((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post |
|
126 | ((?:>)?|[^[:alnum:]_\=\/;\(\)]*?) # post | |
111 | (?=<|\s|$) |
|
127 | (?=<|\s|$) | |
112 | }x unless const_defined?(:AUTO_LINK_RE) |
|
128 | }x unless const_defined?(:AUTO_LINK_RE) | |
113 |
|
129 | |||
114 | # Destructively replaces urls into clickable links |
|
130 | # Destructively replaces urls into clickable links | |
115 | def auto_link!(text) |
|
131 | def auto_link!(text) | |
116 | text.gsub!(AUTO_LINK_RE) do |
|
132 | text.gsub!(AUTO_LINK_RE) do | |
117 | all, leading, proto, url, post = $&, $1, $2, $3, $6 |
|
133 | all, leading, proto, url, post = $&, $1, $2, $3, $6 | |
118 | if leading =~ /<a\s/i || leading =~ /![<>=]?/ |
|
134 | if leading =~ /<a\s/i || leading =~ /![<>=]?/ | |
119 | # don't replace URLs that are already linked |
|
135 | # don't replace URLs that are already linked | |
120 | # and URLs prefixed with ! !> !< != (textile images) |
|
136 | # and URLs prefixed with ! !> !< != (textile images) | |
121 | all |
|
137 | all | |
122 | else |
|
138 | else | |
123 | # Idea below : an URL with unbalanced parenthesis and |
|
139 | # Idea below : an URL with unbalanced parenthesis and | |
124 | # ending by ')' is put into external parenthesis |
|
140 | # ending by ')' is put into external parenthesis | |
125 | if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) ) |
|
141 | if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) ) | |
126 | url=url[0..-2] # discard closing parenthesis from url |
|
142 | url=url[0..-2] # discard closing parenthesis from url | |
127 | post = ")"+post # add closing parenthesis to post |
|
143 | post = ")"+post # add closing parenthesis to post | |
128 | end |
|
144 | end | |
129 | content = proto + url |
|
145 | content = proto + url | |
130 | href = "#{proto=="www."?"http://www.":proto}#{url}" |
|
146 | href = "#{proto=="www."?"http://www.":proto}#{url}" | |
131 | %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe |
|
147 | %(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe | |
132 | end |
|
148 | end | |
133 | end |
|
149 | end | |
134 | end |
|
150 | end | |
135 |
|
151 | |||
136 | # Destructively replaces email addresses into clickable links |
|
152 | # Destructively replaces email addresses into clickable links | |
137 | def auto_mailto!(text) |
|
153 | def auto_mailto!(text) | |
138 | text.gsub!(/([\w\.!#\$%\-+.\/]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do |
|
154 | text.gsub!(/([\w\.!#\$%\-+.\/]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do | |
139 | mail = $1 |
|
155 | mail = $1 | |
140 | if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) |
|
156 | if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) | |
141 |
|
157 | |||
142 | else |
|
158 | else | |
143 | %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe |
|
159 | %(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe | |
144 | end |
|
160 | end | |
145 | end |
|
161 | end | |
146 | end |
|
162 | end | |
147 | end |
|
163 | end | |
148 |
|
164 | |||
149 | # Default formatter module |
|
165 | # Default formatter module | |
150 | module NullFormatter |
|
166 | module NullFormatter | |
151 | class Formatter |
|
167 | class Formatter | |
152 | include ActionView::Helpers::TagHelper |
|
168 | include ActionView::Helpers::TagHelper | |
153 | include ActionView::Helpers::TextHelper |
|
169 | include ActionView::Helpers::TextHelper | |
154 | include ActionView::Helpers::UrlHelper |
|
170 | include ActionView::Helpers::UrlHelper | |
155 | include Redmine::WikiFormatting::LinksHelper |
|
171 | include Redmine::WikiFormatting::LinksHelper | |
156 |
|
172 | |||
157 | def initialize(text) |
|
173 | def initialize(text) | |
158 | @text = text |
|
174 | @text = text | |
159 | end |
|
175 | end | |
160 |
|
176 | |||
161 | def to_html(*args) |
|
177 | def to_html(*args) | |
162 | t = CGI::escapeHTML(@text) |
|
178 | t = CGI::escapeHTML(@text) | |
163 | auto_link!(t) |
|
179 | auto_link!(t) | |
164 | auto_mailto!(t) |
|
180 | auto_mailto!(t) | |
165 | simple_format(t, {}, :sanitize => false) |
|
181 | simple_format(t, {}, :sanitize => false) | |
166 | end |
|
182 | end | |
167 | end |
|
183 | end | |
168 |
|
184 | |||
169 | module Helper |
|
185 | module Helper | |
170 | def wikitoolbar_for(field_id) |
|
186 | def wikitoolbar_for(field_id) | |
171 | end |
|
187 | end | |
172 |
|
188 | |||
173 | def heads_for_wiki_formatter |
|
189 | def heads_for_wiki_formatter | |
174 | end |
|
190 | end | |
175 |
|
191 | |||
176 | def initial_page_content(page) |
|
192 | def initial_page_content(page) | |
177 | page.pretty_title.to_s |
|
193 | page.pretty_title.to_s | |
178 | end |
|
194 | end | |
179 | end |
|
195 | end | |
180 | end |
|
196 | end | |
181 | end |
|
197 | end | |
182 | end |
|
198 | end |
@@ -1,22 +1,23 | |||||
1 | x-sender: <jsmith@somenet.foo> |
|
1 | x-sender: <jsmith@somenet.foo> | |
2 | x-receiver: <redmine@somenet.foo> |
|
2 | x-receiver: <redmine@somenet.foo> | |
3 | Received: from [127.0.0.1] ([127.0.0.1]) by somenet.foo with Quick 'n Easy Mail Server SMTP (1.0.0.0); |
|
3 | Received: from [127.0.0.1] ([127.0.0.1]) by somenet.foo with Quick 'n Easy Mail Server SMTP (1.0.0.0); | |
4 | Sun, 14 Dec 2008 16:18:06 GMT |
|
4 | Sun, 14 Dec 2008 16:18:06 GMT | |
5 | Message-ID: <494531B9.1070709@somenet.foo> |
|
5 | Message-ID: <494531B9.1070709@somenet.foo> | |
6 | Date: Sun, 14 Dec 2008 17:18:01 +0100 |
|
6 | Date: Sun, 14 Dec 2008 17:18:01 +0100 | |
7 | From: "John Smith" <jsmith@somenet.foo> |
|
7 | From: "John Smith" <jsmith@somenet.foo> | |
8 | User-Agent: Thunderbird 2.0.0.18 (Windows/20081105) |
|
8 | User-Agent: Thunderbird 2.0.0.18 (Windows/20081105) | |
9 | MIME-Version: 1.0 |
|
9 | MIME-Version: 1.0 | |
10 | To: redmine@somenet.foo |
|
10 | To: redmine@somenet.foo | |
11 | Subject: HTML email |
|
11 | Subject: HTML email | |
12 | Content-Type: text/html; charset=ISO-8859-1 |
|
12 | Content-Type: text/html; charset=ISO-8859-1 | |
13 | Content-Transfer-Encoding: 7bit |
|
13 | Content-Transfer-Encoding: 7bit | |
14 |
|
14 | |||
15 | <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> |
|
15 | <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> | |
16 | <html> |
|
16 | <html> | |
17 | <head> |
|
17 | <head> | |
|
18 | <style>p {font-size:12.0pt;}</style> | |||
18 | </head> |
|
19 | </head> | |
19 | <body bgcolor="#ffffff" text="#000000"> |
|
20 | <body bgcolor="#ffffff" text="#000000"> | |
20 | This is a <b>html-only</b> email.<br> |
|
21 | This is a <b>html-only</b> email.<br><h1>With a title</h1><p>and a paragraph.</p> | |
21 | </body> |
|
22 | </body> | |
22 | </html> |
|
23 | </html> |
@@ -1,977 +1,997 | |||||
1 | # encoding: utf-8 |
|
1 | # encoding: utf-8 | |
2 | # |
|
2 | # | |
3 | # Redmine - project management software |
|
3 | # Redmine - project management software | |
4 | # Copyright (C) 2006-2015 Jean-Philippe Lang |
|
4 | # Copyright (C) 2006-2015 Jean-Philippe Lang | |
5 | # |
|
5 | # | |
6 | # This program is free software; you can redistribute it and/or |
|
6 | # This program is free software; you can redistribute it and/or | |
7 | # modify it under the terms of the GNU General Public License |
|
7 | # modify it under the terms of the GNU General Public License | |
8 | # as published by the Free Software Foundation; either version 2 |
|
8 | # as published by the Free Software Foundation; either version 2 | |
9 | # of the License, or (at your option) any later version. |
|
9 | # of the License, or (at your option) any later version. | |
10 | # |
|
10 | # | |
11 | # This program is distributed in the hope that it will be useful, |
|
11 | # This program is distributed in the hope that it will be useful, | |
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | # GNU General Public License for more details. |
|
14 | # GNU General Public License for more details. | |
15 | # |
|
15 | # | |
16 | # You should have received a copy of the GNU General Public License |
|
16 | # You should have received a copy of the GNU General Public License | |
17 | # along with this program; if not, write to the Free Software |
|
17 | # along with this program; if not, write to the Free Software | |
18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
19 |
|
19 | |||
20 | require File.expand_path('../../test_helper', __FILE__) |
|
20 | require File.expand_path('../../test_helper', __FILE__) | |
21 |
|
21 | |||
22 | class MailHandlerTest < ActiveSupport::TestCase |
|
22 | class MailHandlerTest < ActiveSupport::TestCase | |
23 | fixtures :users, :projects, :enabled_modules, :roles, |
|
23 | fixtures :users, :projects, :enabled_modules, :roles, | |
24 | :members, :member_roles, :users, |
|
24 | :members, :member_roles, :users, | |
25 | :email_addresses, |
|
25 | :email_addresses, | |
26 | :issues, :issue_statuses, |
|
26 | :issues, :issue_statuses, | |
27 | :workflows, :trackers, :projects_trackers, |
|
27 | :workflows, :trackers, :projects_trackers, | |
28 | :versions, :enumerations, :issue_categories, |
|
28 | :versions, :enumerations, :issue_categories, | |
29 | :custom_fields, :custom_fields_trackers, :custom_fields_projects, |
|
29 | :custom_fields, :custom_fields_trackers, :custom_fields_projects, | |
30 | :boards, :messages |
|
30 | :boards, :messages | |
31 |
|
31 | |||
32 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' |
|
32 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' | |
33 |
|
33 | |||
34 | def setup |
|
34 | def setup | |
35 | ActionMailer::Base.deliveries.clear |
|
35 | ActionMailer::Base.deliveries.clear | |
36 | Setting.notified_events = Redmine::Notifiable.all.collect(&:name) |
|
36 | Setting.notified_events = Redmine::Notifiable.all.collect(&:name) | |
37 | end |
|
37 | end | |
38 |
|
38 | |||
39 | def teardown |
|
39 | def teardown | |
40 | Setting.clear_cache |
|
40 | Setting.clear_cache | |
41 | end |
|
41 | end | |
42 |
|
42 | |||
43 | def test_add_issue |
|
43 | def test_add_issue | |
44 | ActionMailer::Base.deliveries.clear |
|
44 | ActionMailer::Base.deliveries.clear | |
45 | lft1 = new_issue_lft |
|
45 | lft1 = new_issue_lft | |
46 | # This email contains: 'Project: onlinestore' |
|
46 | # This email contains: 'Project: onlinestore' | |
47 | issue = submit_email('ticket_on_given_project.eml') |
|
47 | issue = submit_email('ticket_on_given_project.eml') | |
48 | assert issue.is_a?(Issue) |
|
48 | assert issue.is_a?(Issue) | |
49 | assert !issue.new_record? |
|
49 | assert !issue.new_record? | |
50 | issue.reload |
|
50 | issue.reload | |
51 | assert_equal Project.find(2), issue.project |
|
51 | assert_equal Project.find(2), issue.project | |
52 | assert_equal issue.project.trackers.first, issue.tracker |
|
52 | assert_equal issue.project.trackers.first, issue.tracker | |
53 | assert_equal 'New ticket on a given project', issue.subject |
|
53 | assert_equal 'New ticket on a given project', issue.subject | |
54 | assert_equal User.find_by_login('jsmith'), issue.author |
|
54 | assert_equal User.find_by_login('jsmith'), issue.author | |
55 | assert_equal IssueStatus.find_by_name('Resolved'), issue.status |
|
55 | assert_equal IssueStatus.find_by_name('Resolved'), issue.status | |
56 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
56 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
57 | assert_equal '2010-01-01', issue.start_date.to_s |
|
57 | assert_equal '2010-01-01', issue.start_date.to_s | |
58 | assert_equal '2010-12-31', issue.due_date.to_s |
|
58 | assert_equal '2010-12-31', issue.due_date.to_s | |
59 | assert_equal User.find_by_login('jsmith'), issue.assigned_to |
|
59 | assert_equal User.find_by_login('jsmith'), issue.assigned_to | |
60 | assert_equal Version.find_by_name('Alpha'), issue.fixed_version |
|
60 | assert_equal Version.find_by_name('Alpha'), issue.fixed_version | |
61 | assert_equal 2.5, issue.estimated_hours |
|
61 | assert_equal 2.5, issue.estimated_hours | |
62 | assert_equal 30, issue.done_ratio |
|
62 | assert_equal 30, issue.done_ratio | |
63 | assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] |
|
63 | assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] | |
64 | # keywords should be removed from the email body |
|
64 | # keywords should be removed from the email body | |
65 | assert !issue.description.match(/^Project:/i) |
|
65 | assert !issue.description.match(/^Project:/i) | |
66 | assert !issue.description.match(/^Status:/i) |
|
66 | assert !issue.description.match(/^Status:/i) | |
67 | assert !issue.description.match(/^Start Date:/i) |
|
67 | assert !issue.description.match(/^Start Date:/i) | |
68 | # Email notification should be sent |
|
68 | # Email notification should be sent | |
69 | mail = ActionMailer::Base.deliveries.last |
|
69 | mail = ActionMailer::Base.deliveries.last | |
70 | assert_not_nil mail |
|
70 | assert_not_nil mail | |
71 | assert mail.subject.include?("##{issue.id}") |
|
71 | assert mail.subject.include?("##{issue.id}") | |
72 | assert mail.subject.include?('New ticket on a given project') |
|
72 | assert mail.subject.include?('New ticket on a given project') | |
73 | end |
|
73 | end | |
74 |
|
74 | |||
75 | def test_add_issue_with_default_tracker |
|
75 | def test_add_issue_with_default_tracker | |
76 | # This email contains: 'Project: onlinestore' |
|
76 | # This email contains: 'Project: onlinestore' | |
77 | issue = submit_email( |
|
77 | issue = submit_email( | |
78 | 'ticket_on_given_project.eml', |
|
78 | 'ticket_on_given_project.eml', | |
79 | :issue => {:tracker => 'Support request'} |
|
79 | :issue => {:tracker => 'Support request'} | |
80 | ) |
|
80 | ) | |
81 | assert issue.is_a?(Issue) |
|
81 | assert issue.is_a?(Issue) | |
82 | assert !issue.new_record? |
|
82 | assert !issue.new_record? | |
83 | issue.reload |
|
83 | issue.reload | |
84 | assert_equal 'Support request', issue.tracker.name |
|
84 | assert_equal 'Support request', issue.tracker.name | |
85 | end |
|
85 | end | |
86 |
|
86 | |||
87 | def test_add_issue_with_status |
|
87 | def test_add_issue_with_status | |
88 | # This email contains: 'Project: onlinestore' and 'Status: Resolved' |
|
88 | # This email contains: 'Project: onlinestore' and 'Status: Resolved' | |
89 | issue = submit_email('ticket_on_given_project.eml') |
|
89 | issue = submit_email('ticket_on_given_project.eml') | |
90 | assert issue.is_a?(Issue) |
|
90 | assert issue.is_a?(Issue) | |
91 | assert !issue.new_record? |
|
91 | assert !issue.new_record? | |
92 | issue.reload |
|
92 | issue.reload | |
93 | assert_equal Project.find(2), issue.project |
|
93 | assert_equal Project.find(2), issue.project | |
94 | assert_equal IssueStatus.find_by_name("Resolved"), issue.status |
|
94 | assert_equal IssueStatus.find_by_name("Resolved"), issue.status | |
95 | end |
|
95 | end | |
96 |
|
96 | |||
97 | def test_add_issue_should_accept_is_private_attribute |
|
97 | def test_add_issue_should_accept_is_private_attribute | |
98 | issue = submit_email('ticket_on_given_project.eml', :issue => {:is_private => '1'}) |
|
98 | issue = submit_email('ticket_on_given_project.eml', :issue => {:is_private => '1'}) | |
99 | assert issue.is_a?(Issue) |
|
99 | assert issue.is_a?(Issue) | |
100 | assert !issue.new_record? |
|
100 | assert !issue.new_record? | |
101 | assert_equal true, issue.reload.is_private |
|
101 | assert_equal true, issue.reload.is_private | |
102 | end |
|
102 | end | |
103 |
|
103 | |||
104 | def test_add_issue_with_attributes_override |
|
104 | def test_add_issue_with_attributes_override | |
105 | issue = submit_email( |
|
105 | issue = submit_email( | |
106 | 'ticket_with_attributes.eml', |
|
106 | 'ticket_with_attributes.eml', | |
107 | :allow_override => 'tracker,category,priority' |
|
107 | :allow_override => 'tracker,category,priority' | |
108 | ) |
|
108 | ) | |
109 | assert issue.is_a?(Issue) |
|
109 | assert issue.is_a?(Issue) | |
110 | assert !issue.new_record? |
|
110 | assert !issue.new_record? | |
111 | issue.reload |
|
111 | issue.reload | |
112 | assert_equal 'New ticket on a given project', issue.subject |
|
112 | assert_equal 'New ticket on a given project', issue.subject | |
113 | assert_equal User.find_by_login('jsmith'), issue.author |
|
113 | assert_equal User.find_by_login('jsmith'), issue.author | |
114 | assert_equal Project.find(2), issue.project |
|
114 | assert_equal Project.find(2), issue.project | |
115 | assert_equal 'Feature request', issue.tracker.to_s |
|
115 | assert_equal 'Feature request', issue.tracker.to_s | |
116 | assert_equal 'Stock management', issue.category.to_s |
|
116 | assert_equal 'Stock management', issue.category.to_s | |
117 | assert_equal 'Urgent', issue.priority.to_s |
|
117 | assert_equal 'Urgent', issue.priority.to_s | |
118 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
118 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
119 | end |
|
119 | end | |
120 |
|
120 | |||
121 | def test_add_issue_with_group_assignment |
|
121 | def test_add_issue_with_group_assignment | |
122 | with_settings :issue_group_assignment => '1' do |
|
122 | with_settings :issue_group_assignment => '1' do | |
123 | issue = submit_email('ticket_on_given_project.eml') do |email| |
|
123 | issue = submit_email('ticket_on_given_project.eml') do |email| | |
124 | email.gsub!('Assigned to: John Smith', 'Assigned to: B Team') |
|
124 | email.gsub!('Assigned to: John Smith', 'Assigned to: B Team') | |
125 | end |
|
125 | end | |
126 | assert issue.is_a?(Issue) |
|
126 | assert issue.is_a?(Issue) | |
127 | assert !issue.new_record? |
|
127 | assert !issue.new_record? | |
128 | issue.reload |
|
128 | issue.reload | |
129 | assert_equal Group.find(11), issue.assigned_to |
|
129 | assert_equal Group.find(11), issue.assigned_to | |
130 | end |
|
130 | end | |
131 | end |
|
131 | end | |
132 |
|
132 | |||
133 | def test_add_issue_with_partial_attributes_override |
|
133 | def test_add_issue_with_partial_attributes_override | |
134 | issue = submit_email( |
|
134 | issue = submit_email( | |
135 | 'ticket_with_attributes.eml', |
|
135 | 'ticket_with_attributes.eml', | |
136 | :issue => {:priority => 'High'}, |
|
136 | :issue => {:priority => 'High'}, | |
137 | :allow_override => ['tracker'] |
|
137 | :allow_override => ['tracker'] | |
138 | ) |
|
138 | ) | |
139 | assert issue.is_a?(Issue) |
|
139 | assert issue.is_a?(Issue) | |
140 | assert !issue.new_record? |
|
140 | assert !issue.new_record? | |
141 | issue.reload |
|
141 | issue.reload | |
142 | assert_equal 'New ticket on a given project', issue.subject |
|
142 | assert_equal 'New ticket on a given project', issue.subject | |
143 | assert_equal User.find_by_login('jsmith'), issue.author |
|
143 | assert_equal User.find_by_login('jsmith'), issue.author | |
144 | assert_equal Project.find(2), issue.project |
|
144 | assert_equal Project.find(2), issue.project | |
145 | assert_equal 'Feature request', issue.tracker.to_s |
|
145 | assert_equal 'Feature request', issue.tracker.to_s | |
146 | assert_nil issue.category |
|
146 | assert_nil issue.category | |
147 | assert_equal 'High', issue.priority.to_s |
|
147 | assert_equal 'High', issue.priority.to_s | |
148 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
148 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
149 | end |
|
149 | end | |
150 |
|
150 | |||
151 | def test_add_issue_with_spaces_between_attribute_and_separator |
|
151 | def test_add_issue_with_spaces_between_attribute_and_separator | |
152 | issue = submit_email( |
|
152 | issue = submit_email( | |
153 | 'ticket_with_spaces_between_attribute_and_separator.eml', |
|
153 | 'ticket_with_spaces_between_attribute_and_separator.eml', | |
154 | :allow_override => 'tracker,category,priority' |
|
154 | :allow_override => 'tracker,category,priority' | |
155 | ) |
|
155 | ) | |
156 | assert issue.is_a?(Issue) |
|
156 | assert issue.is_a?(Issue) | |
157 | assert !issue.new_record? |
|
157 | assert !issue.new_record? | |
158 | issue.reload |
|
158 | issue.reload | |
159 | assert_equal 'New ticket on a given project', issue.subject |
|
159 | assert_equal 'New ticket on a given project', issue.subject | |
160 | assert_equal User.find_by_login('jsmith'), issue.author |
|
160 | assert_equal User.find_by_login('jsmith'), issue.author | |
161 | assert_equal Project.find(2), issue.project |
|
161 | assert_equal Project.find(2), issue.project | |
162 | assert_equal 'Feature request', issue.tracker.to_s |
|
162 | assert_equal 'Feature request', issue.tracker.to_s | |
163 | assert_equal 'Stock management', issue.category.to_s |
|
163 | assert_equal 'Stock management', issue.category.to_s | |
164 | assert_equal 'Urgent', issue.priority.to_s |
|
164 | assert_equal 'Urgent', issue.priority.to_s | |
165 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
165 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
166 | end |
|
166 | end | |
167 |
|
167 | |||
168 | def test_add_issue_with_attachment_to_specific_project |
|
168 | def test_add_issue_with_attachment_to_specific_project | |
169 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) |
|
169 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) | |
170 | assert issue.is_a?(Issue) |
|
170 | assert issue.is_a?(Issue) | |
171 | assert !issue.new_record? |
|
171 | assert !issue.new_record? | |
172 | issue.reload |
|
172 | issue.reload | |
173 | assert_equal 'Ticket created by email with attachment', issue.subject |
|
173 | assert_equal 'Ticket created by email with attachment', issue.subject | |
174 | assert_equal User.find_by_login('jsmith'), issue.author |
|
174 | assert_equal User.find_by_login('jsmith'), issue.author | |
175 | assert_equal Project.find(2), issue.project |
|
175 | assert_equal Project.find(2), issue.project | |
176 | assert_equal 'This is a new ticket with attachments', issue.description |
|
176 | assert_equal 'This is a new ticket with attachments', issue.description | |
177 | # Attachment properties |
|
177 | # Attachment properties | |
178 | assert_equal 1, issue.attachments.size |
|
178 | assert_equal 1, issue.attachments.size | |
179 | assert_equal 'Paella.jpg', issue.attachments.first.filename |
|
179 | assert_equal 'Paella.jpg', issue.attachments.first.filename | |
180 | assert_equal 'image/jpeg', issue.attachments.first.content_type |
|
180 | assert_equal 'image/jpeg', issue.attachments.first.content_type | |
181 | assert_equal 10790, issue.attachments.first.filesize |
|
181 | assert_equal 10790, issue.attachments.first.filesize | |
182 | end |
|
182 | end | |
183 |
|
183 | |||
184 | def test_add_issue_with_custom_fields |
|
184 | def test_add_issue_with_custom_fields | |
185 | issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'}) |
|
185 | issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'}) | |
186 | assert issue.is_a?(Issue) |
|
186 | assert issue.is_a?(Issue) | |
187 | assert !issue.new_record? |
|
187 | assert !issue.new_record? | |
188 | issue.reload |
|
188 | issue.reload | |
189 | assert_equal 'New ticket with custom field values', issue.subject |
|
189 | assert_equal 'New ticket with custom field values', issue.subject | |
190 | assert_equal 'PostgreSQL', issue.custom_field_value(1) |
|
190 | assert_equal 'PostgreSQL', issue.custom_field_value(1) | |
191 | assert_equal 'Value for a custom field', issue.custom_field_value(2) |
|
191 | assert_equal 'Value for a custom field', issue.custom_field_value(2) | |
192 | assert !issue.description.match(/^searchable field:/i) |
|
192 | assert !issue.description.match(/^searchable field:/i) | |
193 | end |
|
193 | end | |
194 |
|
194 | |||
195 | def test_add_issue_with_version_custom_fields |
|
195 | def test_add_issue_with_version_custom_fields | |
196 | field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3]) |
|
196 | field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3]) | |
197 |
|
197 | |||
198 | issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email| |
|
198 | issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email| | |
199 | email << "Affected version: 1.0\n" |
|
199 | email << "Affected version: 1.0\n" | |
200 | end |
|
200 | end | |
201 | assert issue.is_a?(Issue) |
|
201 | assert issue.is_a?(Issue) | |
202 | assert !issue.new_record? |
|
202 | assert !issue.new_record? | |
203 | issue.reload |
|
203 | issue.reload | |
204 | assert_equal '2', issue.custom_field_value(field) |
|
204 | assert_equal '2', issue.custom_field_value(field) | |
205 | end |
|
205 | end | |
206 |
|
206 | |||
207 | def test_add_issue_should_match_assignee_on_display_name |
|
207 | def test_add_issue_should_match_assignee_on_display_name | |
208 | user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz') |
|
208 | user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz') | |
209 | User.add_to_project(user, Project.find(2)) |
|
209 | User.add_to_project(user, Project.find(2)) | |
210 | issue = submit_email('ticket_on_given_project.eml') do |email| |
|
210 | issue = submit_email('ticket_on_given_project.eml') do |email| | |
211 | email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz') |
|
211 | email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz') | |
212 | end |
|
212 | end | |
213 | assert issue.is_a?(Issue) |
|
213 | assert issue.is_a?(Issue) | |
214 | assert_equal user, issue.assigned_to |
|
214 | assert_equal user, issue.assigned_to | |
215 | end |
|
215 | end | |
216 |
|
216 | |||
217 | def test_add_issue_should_set_default_start_date |
|
217 | def test_add_issue_should_set_default_start_date | |
218 | with_settings :default_issue_start_date_to_creation_date => '1' do |
|
218 | with_settings :default_issue_start_date_to_creation_date => '1' do | |
219 | issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) |
|
219 | issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) | |
220 | assert issue.is_a?(Issue) |
|
220 | assert issue.is_a?(Issue) | |
221 | assert_equal Date.today, issue.start_date |
|
221 | assert_equal Date.today, issue.start_date | |
222 | end |
|
222 | end | |
223 | end |
|
223 | end | |
224 |
|
224 | |||
225 | def test_add_issue_with_cc |
|
225 | def test_add_issue_with_cc | |
226 | issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) |
|
226 | issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'}) | |
227 | assert issue.is_a?(Issue) |
|
227 | assert issue.is_a?(Issue) | |
228 | assert !issue.new_record? |
|
228 | assert !issue.new_record? | |
229 | issue.reload |
|
229 | issue.reload | |
230 | assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo')) |
|
230 | assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo')) | |
231 | assert_equal 1, issue.watcher_user_ids.size |
|
231 | assert_equal 1, issue.watcher_user_ids.size | |
232 | end |
|
232 | end | |
233 |
|
233 | |||
234 | def test_add_issue_from_additional_email_address |
|
234 | def test_add_issue_from_additional_email_address | |
235 | user = User.find(2) |
|
235 | user = User.find(2) | |
236 | user.mail = 'mainaddress@somenet.foo' |
|
236 | user.mail = 'mainaddress@somenet.foo' | |
237 | user.save! |
|
237 | user.save! | |
238 | EmailAddress.create!(:user => user, :address => 'jsmith@somenet.foo') |
|
238 | EmailAddress.create!(:user => user, :address => 'jsmith@somenet.foo') | |
239 |
|
239 | |||
240 | issue = submit_email('ticket_on_given_project.eml') |
|
240 | issue = submit_email('ticket_on_given_project.eml') | |
241 | assert issue |
|
241 | assert issue | |
242 | assert_equal user, issue.author |
|
242 | assert_equal user, issue.author | |
243 | end |
|
243 | end | |
244 |
|
244 | |||
245 | def test_add_issue_by_unknown_user |
|
245 | def test_add_issue_by_unknown_user | |
246 | assert_no_difference 'User.count' do |
|
246 | assert_no_difference 'User.count' do | |
247 | assert_equal false, |
|
247 | assert_equal false, | |
248 | submit_email( |
|
248 | submit_email( | |
249 | 'ticket_by_unknown_user.eml', |
|
249 | 'ticket_by_unknown_user.eml', | |
250 | :issue => {:project => 'ecookbook'} |
|
250 | :issue => {:project => 'ecookbook'} | |
251 | ) |
|
251 | ) | |
252 | end |
|
252 | end | |
253 | end |
|
253 | end | |
254 |
|
254 | |||
255 | def test_add_issue_by_anonymous_user |
|
255 | def test_add_issue_by_anonymous_user | |
256 | Role.anonymous.add_permission!(:add_issues) |
|
256 | Role.anonymous.add_permission!(:add_issues) | |
257 | assert_no_difference 'User.count' do |
|
257 | assert_no_difference 'User.count' do | |
258 | issue = submit_email( |
|
258 | issue = submit_email( | |
259 | 'ticket_by_unknown_user.eml', |
|
259 | 'ticket_by_unknown_user.eml', | |
260 | :issue => {:project => 'ecookbook'}, |
|
260 | :issue => {:project => 'ecookbook'}, | |
261 | :unknown_user => 'accept' |
|
261 | :unknown_user => 'accept' | |
262 | ) |
|
262 | ) | |
263 | assert issue.is_a?(Issue) |
|
263 | assert issue.is_a?(Issue) | |
264 | assert issue.author.anonymous? |
|
264 | assert issue.author.anonymous? | |
265 | end |
|
265 | end | |
266 | end |
|
266 | end | |
267 |
|
267 | |||
268 | def test_add_issue_by_anonymous_user_with_no_from_address |
|
268 | def test_add_issue_by_anonymous_user_with_no_from_address | |
269 | Role.anonymous.add_permission!(:add_issues) |
|
269 | Role.anonymous.add_permission!(:add_issues) | |
270 | assert_no_difference 'User.count' do |
|
270 | assert_no_difference 'User.count' do | |
271 | issue = submit_email( |
|
271 | issue = submit_email( | |
272 | 'ticket_by_empty_user.eml', |
|
272 | 'ticket_by_empty_user.eml', | |
273 | :issue => {:project => 'ecookbook'}, |
|
273 | :issue => {:project => 'ecookbook'}, | |
274 | :unknown_user => 'accept' |
|
274 | :unknown_user => 'accept' | |
275 | ) |
|
275 | ) | |
276 | assert issue.is_a?(Issue) |
|
276 | assert issue.is_a?(Issue) | |
277 | assert issue.author.anonymous? |
|
277 | assert issue.author.anonymous? | |
278 | end |
|
278 | end | |
279 | end |
|
279 | end | |
280 |
|
280 | |||
281 | def test_add_issue_by_anonymous_user_on_private_project |
|
281 | def test_add_issue_by_anonymous_user_on_private_project | |
282 | Role.anonymous.add_permission!(:add_issues) |
|
282 | Role.anonymous.add_permission!(:add_issues) | |
283 | assert_no_difference 'User.count' do |
|
283 | assert_no_difference 'User.count' do | |
284 | assert_no_difference 'Issue.count' do |
|
284 | assert_no_difference 'Issue.count' do | |
285 | assert_equal false, |
|
285 | assert_equal false, | |
286 | submit_email( |
|
286 | submit_email( | |
287 | 'ticket_by_unknown_user.eml', |
|
287 | 'ticket_by_unknown_user.eml', | |
288 | :issue => {:project => 'onlinestore'}, |
|
288 | :issue => {:project => 'onlinestore'}, | |
289 | :unknown_user => 'accept' |
|
289 | :unknown_user => 'accept' | |
290 | ) |
|
290 | ) | |
291 | end |
|
291 | end | |
292 | end |
|
292 | end | |
293 | end |
|
293 | end | |
294 |
|
294 | |||
295 | def test_add_issue_by_anonymous_user_on_private_project_without_permission_check |
|
295 | def test_add_issue_by_anonymous_user_on_private_project_without_permission_check | |
296 | lft1 = new_issue_lft |
|
296 | lft1 = new_issue_lft | |
297 | assert_no_difference 'User.count' do |
|
297 | assert_no_difference 'User.count' do | |
298 | assert_difference 'Issue.count' do |
|
298 | assert_difference 'Issue.count' do | |
299 | issue = submit_email( |
|
299 | issue = submit_email( | |
300 | 'ticket_by_unknown_user.eml', |
|
300 | 'ticket_by_unknown_user.eml', | |
301 | :issue => {:project => 'onlinestore'}, |
|
301 | :issue => {:project => 'onlinestore'}, | |
302 | :no_permission_check => '1', |
|
302 | :no_permission_check => '1', | |
303 | :unknown_user => 'accept' |
|
303 | :unknown_user => 'accept' | |
304 | ) |
|
304 | ) | |
305 | assert issue.is_a?(Issue) |
|
305 | assert issue.is_a?(Issue) | |
306 | assert issue.author.anonymous? |
|
306 | assert issue.author.anonymous? | |
307 | assert !issue.project.is_public? |
|
307 | assert !issue.project.is_public? | |
308 | assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] |
|
308 | assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt] | |
309 | end |
|
309 | end | |
310 | end |
|
310 | end | |
311 | end |
|
311 | end | |
312 |
|
312 | |||
313 | def test_add_issue_by_created_user |
|
313 | def test_add_issue_by_created_user | |
314 | Setting.default_language = 'en' |
|
314 | Setting.default_language = 'en' | |
315 | assert_difference 'User.count' do |
|
315 | assert_difference 'User.count' do | |
316 | issue = submit_email( |
|
316 | issue = submit_email( | |
317 | 'ticket_by_unknown_user.eml', |
|
317 | 'ticket_by_unknown_user.eml', | |
318 | :issue => {:project => 'ecookbook'}, |
|
318 | :issue => {:project => 'ecookbook'}, | |
319 | :unknown_user => 'create' |
|
319 | :unknown_user => 'create' | |
320 | ) |
|
320 | ) | |
321 | assert issue.is_a?(Issue) |
|
321 | assert issue.is_a?(Issue) | |
322 | assert issue.author.active? |
|
322 | assert issue.author.active? | |
323 | assert_equal 'john.doe@somenet.foo', issue.author.mail |
|
323 | assert_equal 'john.doe@somenet.foo', issue.author.mail | |
324 | assert_equal 'John', issue.author.firstname |
|
324 | assert_equal 'John', issue.author.firstname | |
325 | assert_equal 'Doe', issue.author.lastname |
|
325 | assert_equal 'Doe', issue.author.lastname | |
326 |
|
326 | |||
327 | # account information |
|
327 | # account information | |
328 | email = ActionMailer::Base.deliveries.first |
|
328 | email = ActionMailer::Base.deliveries.first | |
329 | assert_not_nil email |
|
329 | assert_not_nil email | |
330 | assert email.subject.include?('account activation') |
|
330 | assert email.subject.include?('account activation') | |
331 | login = mail_body(email).match(/\* Login: (.*)$/)[1].strip |
|
331 | login = mail_body(email).match(/\* Login: (.*)$/)[1].strip | |
332 | password = mail_body(email).match(/\* Password: (.*)$/)[1].strip |
|
332 | password = mail_body(email).match(/\* Password: (.*)$/)[1].strip | |
333 | assert_equal issue.author, User.try_to_login(login, password) |
|
333 | assert_equal issue.author, User.try_to_login(login, password) | |
334 | end |
|
334 | end | |
335 | end |
|
335 | end | |
336 |
|
336 | |||
337 | def test_created_user_should_be_added_to_groups |
|
337 | def test_created_user_should_be_added_to_groups | |
338 | group1 = Group.generate! |
|
338 | group1 = Group.generate! | |
339 | group2 = Group.generate! |
|
339 | group2 = Group.generate! | |
340 |
|
340 | |||
341 | assert_difference 'User.count' do |
|
341 | assert_difference 'User.count' do | |
342 | submit_email( |
|
342 | submit_email( | |
343 | 'ticket_by_unknown_user.eml', |
|
343 | 'ticket_by_unknown_user.eml', | |
344 | :issue => {:project => 'ecookbook'}, |
|
344 | :issue => {:project => 'ecookbook'}, | |
345 | :unknown_user => 'create', |
|
345 | :unknown_user => 'create', | |
346 | :default_group => "#{group1.name},#{group2.name}" |
|
346 | :default_group => "#{group1.name},#{group2.name}" | |
347 | ) |
|
347 | ) | |
348 | end |
|
348 | end | |
349 | user = User.order('id DESC').first |
|
349 | user = User.order('id DESC').first | |
350 | assert_equal [group1, group2].sort, user.groups.sort |
|
350 | assert_equal [group1, group2].sort, user.groups.sort | |
351 | end |
|
351 | end | |
352 |
|
352 | |||
353 | def test_created_user_should_not_receive_account_information_with_no_account_info_option |
|
353 | def test_created_user_should_not_receive_account_information_with_no_account_info_option | |
354 | assert_difference 'User.count' do |
|
354 | assert_difference 'User.count' do | |
355 | submit_email( |
|
355 | submit_email( | |
356 | 'ticket_by_unknown_user.eml', |
|
356 | 'ticket_by_unknown_user.eml', | |
357 | :issue => {:project => 'ecookbook'}, |
|
357 | :issue => {:project => 'ecookbook'}, | |
358 | :unknown_user => 'create', |
|
358 | :unknown_user => 'create', | |
359 | :no_account_notice => '1' |
|
359 | :no_account_notice => '1' | |
360 | ) |
|
360 | ) | |
361 | end |
|
361 | end | |
362 |
|
362 | |||
363 | # only 1 email for the new issue notification |
|
363 | # only 1 email for the new issue notification | |
364 | assert_equal 1, ActionMailer::Base.deliveries.size |
|
364 | assert_equal 1, ActionMailer::Base.deliveries.size | |
365 | email = ActionMailer::Base.deliveries.first |
|
365 | email = ActionMailer::Base.deliveries.first | |
366 | assert_include 'Ticket by unknown user', email.subject |
|
366 | assert_include 'Ticket by unknown user', email.subject | |
367 | end |
|
367 | end | |
368 |
|
368 | |||
369 | def test_created_user_should_have_mail_notification_to_none_with_no_notification_option |
|
369 | def test_created_user_should_have_mail_notification_to_none_with_no_notification_option | |
370 | assert_difference 'User.count' do |
|
370 | assert_difference 'User.count' do | |
371 | submit_email( |
|
371 | submit_email( | |
372 | 'ticket_by_unknown_user.eml', |
|
372 | 'ticket_by_unknown_user.eml', | |
373 | :issue => {:project => 'ecookbook'}, |
|
373 | :issue => {:project => 'ecookbook'}, | |
374 | :unknown_user => 'create', |
|
374 | :unknown_user => 'create', | |
375 | :no_notification => '1' |
|
375 | :no_notification => '1' | |
376 | ) |
|
376 | ) | |
377 | end |
|
377 | end | |
378 | user = User.order('id DESC').first |
|
378 | user = User.order('id DESC').first | |
379 | assert_equal 'none', user.mail_notification |
|
379 | assert_equal 'none', user.mail_notification | |
380 | end |
|
380 | end | |
381 |
|
381 | |||
382 | def test_add_issue_without_from_header |
|
382 | def test_add_issue_without_from_header | |
383 | Role.anonymous.add_permission!(:add_issues) |
|
383 | Role.anonymous.add_permission!(:add_issues) | |
384 | assert_equal false, submit_email('ticket_without_from_header.eml') |
|
384 | assert_equal false, submit_email('ticket_without_from_header.eml') | |
385 | end |
|
385 | end | |
386 |
|
386 | |||
387 | def test_add_issue_with_invalid_attributes |
|
387 | def test_add_issue_with_invalid_attributes | |
388 | with_settings :default_issue_start_date_to_creation_date => '0' do |
|
388 | with_settings :default_issue_start_date_to_creation_date => '0' do | |
389 | issue = submit_email( |
|
389 | issue = submit_email( | |
390 | 'ticket_with_invalid_attributes.eml', |
|
390 | 'ticket_with_invalid_attributes.eml', | |
391 | :allow_override => 'tracker,category,priority' |
|
391 | :allow_override => 'tracker,category,priority' | |
392 | ) |
|
392 | ) | |
393 | assert issue.is_a?(Issue) |
|
393 | assert issue.is_a?(Issue) | |
394 | assert !issue.new_record? |
|
394 | assert !issue.new_record? | |
395 | issue.reload |
|
395 | issue.reload | |
396 | assert_nil issue.assigned_to |
|
396 | assert_nil issue.assigned_to | |
397 | assert_nil issue.start_date |
|
397 | assert_nil issue.start_date | |
398 | assert_nil issue.due_date |
|
398 | assert_nil issue.due_date | |
399 | assert_equal 0, issue.done_ratio |
|
399 | assert_equal 0, issue.done_ratio | |
400 | assert_equal 'Normal', issue.priority.to_s |
|
400 | assert_equal 'Normal', issue.priority.to_s | |
401 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
401 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
402 | end |
|
402 | end | |
403 | end |
|
403 | end | |
404 |
|
404 | |||
405 | def test_add_issue_with_invalid_project_should_be_assigned_to_default_project |
|
405 | def test_add_issue_with_invalid_project_should_be_assigned_to_default_project | |
406 | issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email| |
|
406 | issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email| | |
407 | email.gsub!(/^Project:.+$/, 'Project: invalid') |
|
407 | email.gsub!(/^Project:.+$/, 'Project: invalid') | |
408 | end |
|
408 | end | |
409 | assert issue.is_a?(Issue) |
|
409 | assert issue.is_a?(Issue) | |
410 | assert !issue.new_record? |
|
410 | assert !issue.new_record? | |
411 | assert_equal 'ecookbook', issue.project.identifier |
|
411 | assert_equal 'ecookbook', issue.project.identifier | |
412 | end |
|
412 | end | |
413 |
|
413 | |||
414 | def test_add_issue_with_localized_attributes |
|
414 | def test_add_issue_with_localized_attributes | |
415 | User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr' |
|
415 | User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr' | |
416 | issue = submit_email( |
|
416 | issue = submit_email( | |
417 | 'ticket_with_localized_attributes.eml', |
|
417 | 'ticket_with_localized_attributes.eml', | |
418 | :allow_override => 'tracker,category,priority' |
|
418 | :allow_override => 'tracker,category,priority' | |
419 | ) |
|
419 | ) | |
420 | assert issue.is_a?(Issue) |
|
420 | assert issue.is_a?(Issue) | |
421 | assert !issue.new_record? |
|
421 | assert !issue.new_record? | |
422 | issue.reload |
|
422 | issue.reload | |
423 | assert_equal 'New ticket on a given project', issue.subject |
|
423 | assert_equal 'New ticket on a given project', issue.subject | |
424 | assert_equal User.find_by_login('jsmith'), issue.author |
|
424 | assert_equal User.find_by_login('jsmith'), issue.author | |
425 | assert_equal Project.find(2), issue.project |
|
425 | assert_equal Project.find(2), issue.project | |
426 | assert_equal 'Feature request', issue.tracker.to_s |
|
426 | assert_equal 'Feature request', issue.tracker.to_s | |
427 | assert_equal 'Stock management', issue.category.to_s |
|
427 | assert_equal 'Stock management', issue.category.to_s | |
428 | assert_equal 'Urgent', issue.priority.to_s |
|
428 | assert_equal 'Urgent', issue.priority.to_s | |
429 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
429 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
430 | end |
|
430 | end | |
431 |
|
431 | |||
432 | def test_add_issue_with_japanese_keywords |
|
432 | def test_add_issue_with_japanese_keywords | |
433 | ja_dev = "\xe9\x96\x8b\xe7\x99\xba".force_encoding('UTF-8') |
|
433 | ja_dev = "\xe9\x96\x8b\xe7\x99\xba".force_encoding('UTF-8') | |
434 | tracker = Tracker.generate!(:name => ja_dev) |
|
434 | tracker = Tracker.generate!(:name => ja_dev) | |
435 | Project.find(1).trackers << tracker |
|
435 | Project.find(1).trackers << tracker | |
436 | issue = submit_email( |
|
436 | issue = submit_email( | |
437 | 'japanese_keywords_iso_2022_jp.eml', |
|
437 | 'japanese_keywords_iso_2022_jp.eml', | |
438 | :issue => {:project => 'ecookbook'}, |
|
438 | :issue => {:project => 'ecookbook'}, | |
439 | :allow_override => 'tracker' |
|
439 | :allow_override => 'tracker' | |
440 | ) |
|
440 | ) | |
441 | assert_kind_of Issue, issue |
|
441 | assert_kind_of Issue, issue | |
442 | assert_equal tracker, issue.tracker |
|
442 | assert_equal tracker, issue.tracker | |
443 | end |
|
443 | end | |
444 |
|
444 | |||
445 | def test_add_issue_from_apple_mail |
|
445 | def test_add_issue_from_apple_mail | |
446 | issue = submit_email( |
|
446 | issue = submit_email( | |
447 | 'apple_mail_with_attachment.eml', |
|
447 | 'apple_mail_with_attachment.eml', | |
448 | :issue => {:project => 'ecookbook'} |
|
448 | :issue => {:project => 'ecookbook'} | |
449 | ) |
|
449 | ) | |
450 | assert_kind_of Issue, issue |
|
450 | assert_kind_of Issue, issue | |
451 | assert_equal 1, issue.attachments.size |
|
451 | assert_equal 1, issue.attachments.size | |
452 |
|
452 | |||
453 | attachment = issue.attachments.first |
|
453 | attachment = issue.attachments.first | |
454 | assert_equal 'paella.jpg', attachment.filename |
|
454 | assert_equal 'paella.jpg', attachment.filename | |
455 | assert_equal 10790, attachment.filesize |
|
455 | assert_equal 10790, attachment.filesize | |
456 | assert File.exist?(attachment.diskfile) |
|
456 | assert File.exist?(attachment.diskfile) | |
457 | assert_equal 10790, File.size(attachment.diskfile) |
|
457 | assert_equal 10790, File.size(attachment.diskfile) | |
458 | assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest |
|
458 | assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest | |
459 | end |
|
459 | end | |
460 |
|
460 | |||
461 | def test_thunderbird_with_attachment_ja |
|
461 | def test_thunderbird_with_attachment_ja | |
462 | issue = submit_email( |
|
462 | issue = submit_email( | |
463 | 'thunderbird_with_attachment_ja.eml', |
|
463 | 'thunderbird_with_attachment_ja.eml', | |
464 | :issue => {:project => 'ecookbook'} |
|
464 | :issue => {:project => 'ecookbook'} | |
465 | ) |
|
465 | ) | |
466 | assert_kind_of Issue, issue |
|
466 | assert_kind_of Issue, issue | |
467 | assert_equal 1, issue.attachments.size |
|
467 | assert_equal 1, issue.attachments.size | |
468 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8') |
|
468 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8') | |
469 | attachment = issue.attachments.first |
|
469 | attachment = issue.attachments.first | |
470 | assert_equal ja, attachment.filename |
|
470 | assert_equal ja, attachment.filename | |
471 | assert_equal 5, attachment.filesize |
|
471 | assert_equal 5, attachment.filesize | |
472 | assert File.exist?(attachment.diskfile) |
|
472 | assert File.exist?(attachment.diskfile) | |
473 | assert_equal 5, File.size(attachment.diskfile) |
|
473 | assert_equal 5, File.size(attachment.diskfile) | |
474 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
474 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest | |
475 | end |
|
475 | end | |
476 |
|
476 | |||
477 | def test_gmail_with_attachment_ja |
|
477 | def test_gmail_with_attachment_ja | |
478 | issue = submit_email( |
|
478 | issue = submit_email( | |
479 | 'gmail_with_attachment_ja.eml', |
|
479 | 'gmail_with_attachment_ja.eml', | |
480 | :issue => {:project => 'ecookbook'} |
|
480 | :issue => {:project => 'ecookbook'} | |
481 | ) |
|
481 | ) | |
482 | assert_kind_of Issue, issue |
|
482 | assert_kind_of Issue, issue | |
483 | assert_equal 1, issue.attachments.size |
|
483 | assert_equal 1, issue.attachments.size | |
484 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8') |
|
484 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8') | |
485 | attachment = issue.attachments.first |
|
485 | attachment = issue.attachments.first | |
486 | assert_equal ja, attachment.filename |
|
486 | assert_equal ja, attachment.filename | |
487 | assert_equal 5, attachment.filesize |
|
487 | assert_equal 5, attachment.filesize | |
488 | assert File.exist?(attachment.diskfile) |
|
488 | assert File.exist?(attachment.diskfile) | |
489 | assert_equal 5, File.size(attachment.diskfile) |
|
489 | assert_equal 5, File.size(attachment.diskfile) | |
490 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
490 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest | |
491 | end |
|
491 | end | |
492 |
|
492 | |||
493 | def test_thunderbird_with_attachment_latin1 |
|
493 | def test_thunderbird_with_attachment_latin1 | |
494 | issue = submit_email( |
|
494 | issue = submit_email( | |
495 | 'thunderbird_with_attachment_iso-8859-1.eml', |
|
495 | 'thunderbird_with_attachment_iso-8859-1.eml', | |
496 | :issue => {:project => 'ecookbook'} |
|
496 | :issue => {:project => 'ecookbook'} | |
497 | ) |
|
497 | ) | |
498 | assert_kind_of Issue, issue |
|
498 | assert_kind_of Issue, issue | |
499 | assert_equal 1, issue.attachments.size |
|
499 | assert_equal 1, issue.attachments.size | |
500 | u = "".force_encoding('UTF-8') |
|
500 | u = "".force_encoding('UTF-8') | |
501 | u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8') |
|
501 | u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8') | |
502 | 11.times { u << u1 } |
|
502 | 11.times { u << u1 } | |
503 | attachment = issue.attachments.first |
|
503 | attachment = issue.attachments.first | |
504 | assert_equal "#{u}.png", attachment.filename |
|
504 | assert_equal "#{u}.png", attachment.filename | |
505 | assert_equal 130, attachment.filesize |
|
505 | assert_equal 130, attachment.filesize | |
506 | assert File.exist?(attachment.diskfile) |
|
506 | assert File.exist?(attachment.diskfile) | |
507 | assert_equal 130, File.size(attachment.diskfile) |
|
507 | assert_equal 130, File.size(attachment.diskfile) | |
508 | assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest |
|
508 | assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest | |
509 | end |
|
509 | end | |
510 |
|
510 | |||
511 | def test_gmail_with_attachment_latin1 |
|
511 | def test_gmail_with_attachment_latin1 | |
512 | issue = submit_email( |
|
512 | issue = submit_email( | |
513 | 'gmail_with_attachment_iso-8859-1.eml', |
|
513 | 'gmail_with_attachment_iso-8859-1.eml', | |
514 | :issue => {:project => 'ecookbook'} |
|
514 | :issue => {:project => 'ecookbook'} | |
515 | ) |
|
515 | ) | |
516 | assert_kind_of Issue, issue |
|
516 | assert_kind_of Issue, issue | |
517 | assert_equal 1, issue.attachments.size |
|
517 | assert_equal 1, issue.attachments.size | |
518 | u = "".force_encoding('UTF-8') |
|
518 | u = "".force_encoding('UTF-8') | |
519 | u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8') |
|
519 | u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8') | |
520 | 11.times { u << u1 } |
|
520 | 11.times { u << u1 } | |
521 | attachment = issue.attachments.first |
|
521 | attachment = issue.attachments.first | |
522 | assert_equal "#{u}.txt", attachment.filename |
|
522 | assert_equal "#{u}.txt", attachment.filename | |
523 | assert_equal 5, attachment.filesize |
|
523 | assert_equal 5, attachment.filesize | |
524 | assert File.exist?(attachment.diskfile) |
|
524 | assert File.exist?(attachment.diskfile) | |
525 | assert_equal 5, File.size(attachment.diskfile) |
|
525 | assert_equal 5, File.size(attachment.diskfile) | |
526 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest |
|
526 | assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest | |
527 | end |
|
527 | end | |
528 |
|
528 | |||
529 | def test_multiple_inline_text_parts_should_be_appended_to_issue_description |
|
529 | def test_multiple_inline_text_parts_should_be_appended_to_issue_description | |
530 | issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) |
|
530 | issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) | |
531 | assert_include 'first', issue.description |
|
531 | assert_include 'first', issue.description | |
532 | assert_include 'second', issue.description |
|
532 | assert_include 'second', issue.description | |
533 | assert_include 'third', issue.description |
|
533 | assert_include 'third', issue.description | |
534 | end |
|
534 | end | |
535 |
|
535 | |||
536 | def test_attachment_text_part_should_be_added_as_issue_attachment |
|
536 | def test_attachment_text_part_should_be_added_as_issue_attachment | |
537 | issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) |
|
537 | issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'}) | |
538 | assert_not_include 'Plain text attachment', issue.description |
|
538 | assert_not_include 'Plain text attachment', issue.description | |
539 | attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'} |
|
539 | attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'} | |
540 | assert_not_nil attachment |
|
540 | assert_not_nil attachment | |
541 | assert_include 'Plain text attachment', File.read(attachment.diskfile) |
|
541 | assert_include 'Plain text attachment', File.read(attachment.diskfile) | |
542 | end |
|
542 | end | |
543 |
|
543 | |||
544 | def test_add_issue_with_iso_8859_1_subject |
|
544 | def test_add_issue_with_iso_8859_1_subject | |
545 | issue = submit_email( |
|
545 | issue = submit_email( | |
546 | 'subject_as_iso-8859-1.eml', |
|
546 | 'subject_as_iso-8859-1.eml', | |
547 | :issue => {:project => 'ecookbook'} |
|
547 | :issue => {:project => 'ecookbook'} | |
548 | ) |
|
548 | ) | |
549 | str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc...".force_encoding('UTF-8') |
|
549 | str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc...".force_encoding('UTF-8') | |
550 | assert_kind_of Issue, issue |
|
550 | assert_kind_of Issue, issue | |
551 | assert_equal str, issue.subject |
|
551 | assert_equal str, issue.subject | |
552 | end |
|
552 | end | |
553 |
|
553 | |||
554 | def test_quoted_printable_utf8 |
|
554 | def test_quoted_printable_utf8 | |
555 | issue = submit_email( |
|
555 | issue = submit_email( | |
556 | 'quoted_printable_utf8.eml', |
|
556 | 'quoted_printable_utf8.eml', | |
557 | :issue => {:project => 'ecookbook'} |
|
557 | :issue => {:project => 'ecookbook'} | |
558 | ) |
|
558 | ) | |
559 | assert_kind_of Issue, issue |
|
559 | assert_kind_of Issue, issue | |
560 | str = "Freundliche Gr\xc3\xbcsse".force_encoding('UTF-8') |
|
560 | str = "Freundliche Gr\xc3\xbcsse".force_encoding('UTF-8') | |
561 | assert_equal str, issue.description |
|
561 | assert_equal str, issue.description | |
562 | end |
|
562 | end | |
563 |
|
563 | |||
564 | def test_gmail_iso8859_2 |
|
564 | def test_gmail_iso8859_2 | |
565 | issue = submit_email( |
|
565 | issue = submit_email( | |
566 | 'gmail-iso8859-2.eml', |
|
566 | 'gmail-iso8859-2.eml', | |
567 | :issue => {:project => 'ecookbook'} |
|
567 | :issue => {:project => 'ecookbook'} | |
568 | ) |
|
568 | ) | |
569 | assert_kind_of Issue, issue |
|
569 | assert_kind_of Issue, issue | |
570 | str = "Na \xc5\xa1triku se su\xc5\xa1i \xc5\xa1osi\xc4\x87.".force_encoding('UTF-8') |
|
570 | str = "Na \xc5\xa1triku se su\xc5\xa1i \xc5\xa1osi\xc4\x87.".force_encoding('UTF-8') | |
571 | assert issue.description.include?(str) |
|
571 | assert issue.description.include?(str) | |
572 | end |
|
572 | end | |
573 |
|
573 | |||
574 | def test_add_issue_with_japanese_subject |
|
574 | def test_add_issue_with_japanese_subject | |
575 | issue = submit_email( |
|
575 | issue = submit_email( | |
576 | 'subject_japanese_1.eml', |
|
576 | 'subject_japanese_1.eml', | |
577 | :issue => {:project => 'ecookbook'} |
|
577 | :issue => {:project => 'ecookbook'} | |
578 | ) |
|
578 | ) | |
579 | assert_kind_of Issue, issue |
|
579 | assert_kind_of Issue, issue | |
580 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8') |
|
580 | ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8') | |
581 | assert_equal ja, issue.subject |
|
581 | assert_equal ja, issue.subject | |
582 | end |
|
582 | end | |
583 |
|
583 | |||
584 | def test_add_issue_with_korean_body |
|
584 | def test_add_issue_with_korean_body | |
585 | # Make sure mail bodies with a charset unknown to Ruby |
|
585 | # Make sure mail bodies with a charset unknown to Ruby | |
586 | # but known to the Mail gem 2.5.4 are handled correctly |
|
586 | # but known to the Mail gem 2.5.4 are handled correctly | |
587 | kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4.".force_encoding('UTF-8') |
|
587 | kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4.".force_encoding('UTF-8') | |
588 | issue = submit_email( |
|
588 | issue = submit_email( | |
589 | 'body_ks_c_5601-1987.eml', |
|
589 | 'body_ks_c_5601-1987.eml', | |
590 | :issue => {:project => 'ecookbook'} |
|
590 | :issue => {:project => 'ecookbook'} | |
591 | ) |
|
591 | ) | |
592 | assert_kind_of Issue, issue |
|
592 | assert_kind_of Issue, issue | |
593 | assert_equal kr, issue.description |
|
593 | assert_equal kr, issue.description | |
594 | end |
|
594 | end | |
595 |
|
595 | |||
596 | def test_add_issue_with_no_subject_header |
|
596 | def test_add_issue_with_no_subject_header | |
597 | issue = submit_email( |
|
597 | issue = submit_email( | |
598 | 'no_subject_header.eml', |
|
598 | 'no_subject_header.eml', | |
599 | :issue => {:project => 'ecookbook'} |
|
599 | :issue => {:project => 'ecookbook'} | |
600 | ) |
|
600 | ) | |
601 | assert_kind_of Issue, issue |
|
601 | assert_kind_of Issue, issue | |
602 | assert_equal '(no subject)', issue.subject |
|
602 | assert_equal '(no subject)', issue.subject | |
603 | end |
|
603 | end | |
604 |
|
604 | |||
605 | def test_add_issue_with_mixed_japanese_subject |
|
605 | def test_add_issue_with_mixed_japanese_subject | |
606 | issue = submit_email( |
|
606 | issue = submit_email( | |
607 | 'subject_japanese_2.eml', |
|
607 | 'subject_japanese_2.eml', | |
608 | :issue => {:project => 'ecookbook'} |
|
608 | :issue => {:project => 'ecookbook'} | |
609 | ) |
|
609 | ) | |
610 | assert_kind_of Issue, issue |
|
610 | assert_kind_of Issue, issue | |
611 | ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8') |
|
611 | ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8') | |
612 | assert_equal ja, issue.subject |
|
612 | assert_equal ja, issue.subject | |
613 | end |
|
613 | end | |
614 |
|
614 | |||
615 | def test_should_ignore_emails_from_locked_users |
|
615 | def test_should_ignore_emails_from_locked_users | |
616 | User.find(2).lock! |
|
616 | User.find(2).lock! | |
617 |
|
617 | |||
618 | MailHandler.any_instance.expects(:dispatch).never |
|
618 | MailHandler.any_instance.expects(:dispatch).never | |
619 | assert_no_difference 'Issue.count' do |
|
619 | assert_no_difference 'Issue.count' do | |
620 | assert_equal false, submit_email('ticket_on_given_project.eml') |
|
620 | assert_equal false, submit_email('ticket_on_given_project.eml') | |
621 | end |
|
621 | end | |
622 | end |
|
622 | end | |
623 |
|
623 | |||
624 | def test_should_ignore_emails_from_emission_address |
|
624 | def test_should_ignore_emails_from_emission_address | |
625 | Role.anonymous.add_permission!(:add_issues) |
|
625 | Role.anonymous.add_permission!(:add_issues) | |
626 | assert_no_difference 'User.count' do |
|
626 | assert_no_difference 'User.count' do | |
627 | assert_equal false, |
|
627 | assert_equal false, | |
628 | submit_email( |
|
628 | submit_email( | |
629 | 'ticket_from_emission_address.eml', |
|
629 | 'ticket_from_emission_address.eml', | |
630 | :issue => {:project => 'ecookbook'}, |
|
630 | :issue => {:project => 'ecookbook'}, | |
631 | :unknown_user => 'create' |
|
631 | :unknown_user => 'create' | |
632 | ) |
|
632 | ) | |
633 | end |
|
633 | end | |
634 | end |
|
634 | end | |
635 |
|
635 | |||
636 | def test_should_ignore_auto_replied_emails |
|
636 | def test_should_ignore_auto_replied_emails | |
637 | MailHandler.any_instance.expects(:dispatch).never |
|
637 | MailHandler.any_instance.expects(:dispatch).never | |
638 | [ |
|
638 | [ | |
639 | "Auto-Submitted: auto-replied", |
|
639 | "Auto-Submitted: auto-replied", | |
640 | "Auto-Submitted: Auto-Replied", |
|
640 | "Auto-Submitted: Auto-Replied", | |
641 | "Auto-Submitted: auto-generated", |
|
641 | "Auto-Submitted: auto-generated", | |
642 | 'X-Autoreply: yes' |
|
642 | 'X-Autoreply: yes' | |
643 | ].each do |header| |
|
643 | ].each do |header| | |
644 | raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) |
|
644 | raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) | |
645 | raw = header + "\n" + raw |
|
645 | raw = header + "\n" + raw | |
646 |
|
646 | |||
647 | assert_no_difference 'Issue.count' do |
|
647 | assert_no_difference 'Issue.count' do | |
648 | assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored" |
|
648 | assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored" | |
649 | end |
|
649 | end | |
650 | end |
|
650 | end | |
651 | end |
|
651 | end | |
652 |
|
652 | |||
653 | test "should not ignore Auto-Submitted headers not defined in RFC3834" do |
|
653 | test "should not ignore Auto-Submitted headers not defined in RFC3834" do | |
654 | [ |
|
654 | [ | |
655 | "Auto-Submitted: auto-forwarded" |
|
655 | "Auto-Submitted: auto-forwarded" | |
656 | ].each do |header| |
|
656 | ].each do |header| | |
657 | raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) |
|
657 | raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml')) | |
658 | raw = header + "\n" + raw |
|
658 | raw = header + "\n" + raw | |
659 |
|
659 | |||
660 | assert_difference 'Issue.count', 1 do |
|
660 | assert_difference 'Issue.count', 1 do | |
661 | assert_not_nil MailHandler.receive(raw), "email with #{header} header was ignored" |
|
661 | assert_not_nil MailHandler.receive(raw), "email with #{header} header was ignored" | |
662 | end |
|
662 | end | |
663 | end |
|
663 | end | |
664 | end |
|
664 | end | |
665 |
|
665 | |||
666 | def test_add_issue_should_send_email_notification |
|
666 | def test_add_issue_should_send_email_notification | |
667 | Setting.notified_events = ['issue_added'] |
|
667 | Setting.notified_events = ['issue_added'] | |
668 | ActionMailer::Base.deliveries.clear |
|
668 | ActionMailer::Base.deliveries.clear | |
669 | # This email contains: 'Project: onlinestore' |
|
669 | # This email contains: 'Project: onlinestore' | |
670 | issue = submit_email('ticket_on_given_project.eml') |
|
670 | issue = submit_email('ticket_on_given_project.eml') | |
671 | assert issue.is_a?(Issue) |
|
671 | assert issue.is_a?(Issue) | |
672 | assert_equal 1, ActionMailer::Base.deliveries.size |
|
672 | assert_equal 1, ActionMailer::Base.deliveries.size | |
673 | end |
|
673 | end | |
674 |
|
674 | |||
675 | def test_update_issue |
|
675 | def test_update_issue | |
676 | journal = submit_email('ticket_reply.eml') |
|
676 | journal = submit_email('ticket_reply.eml') | |
677 | assert journal.is_a?(Journal) |
|
677 | assert journal.is_a?(Journal) | |
678 | assert_equal User.find_by_login('jsmith'), journal.user |
|
678 | assert_equal User.find_by_login('jsmith'), journal.user | |
679 | assert_equal Issue.find(2), journal.journalized |
|
679 | assert_equal Issue.find(2), journal.journalized | |
680 | assert_match /This is reply/, journal.notes |
|
680 | assert_match /This is reply/, journal.notes | |
681 | assert_equal false, journal.private_notes |
|
681 | assert_equal false, journal.private_notes | |
682 | assert_equal 'Feature request', journal.issue.tracker.name |
|
682 | assert_equal 'Feature request', journal.issue.tracker.name | |
683 | end |
|
683 | end | |
684 |
|
684 | |||
685 | def test_update_issue_should_accept_issue_id_after_space_inside_brackets |
|
685 | def test_update_issue_should_accept_issue_id_after_space_inside_brackets | |
686 | journal = submit_email('ticket_reply_with_status.eml') do |email| |
|
686 | journal = submit_email('ticket_reply_with_status.eml') do |email| | |
687 | assert email.sub!(/^Subject:.*$/, "Subject: Re: [Feature request #2] Add ingredients categories") |
|
687 | assert email.sub!(/^Subject:.*$/, "Subject: Re: [Feature request #2] Add ingredients categories") | |
688 | end |
|
688 | end | |
689 | assert journal.is_a?(Journal) |
|
689 | assert journal.is_a?(Journal) | |
690 | assert_equal Issue.find(2), journal.journalized |
|
690 | assert_equal Issue.find(2), journal.journalized | |
691 | end |
|
691 | end | |
692 |
|
692 | |||
693 | def test_update_issue_should_accept_issue_id_inside_brackets |
|
693 | def test_update_issue_should_accept_issue_id_inside_brackets | |
694 | journal = submit_email('ticket_reply_with_status.eml') do |email| |
|
694 | journal = submit_email('ticket_reply_with_status.eml') do |email| | |
695 | assert email.sub!(/^Subject:.*$/, "Subject: Re: [#2] Add ingredients categories") |
|
695 | assert email.sub!(/^Subject:.*$/, "Subject: Re: [#2] Add ingredients categories") | |
696 | end |
|
696 | end | |
697 | assert journal.is_a?(Journal) |
|
697 | assert journal.is_a?(Journal) | |
698 | assert_equal Issue.find(2), journal.journalized |
|
698 | assert_equal Issue.find(2), journal.journalized | |
699 | end |
|
699 | end | |
700 |
|
700 | |||
701 | def test_update_issue_should_ignore_bogus_issue_ids_in_subject |
|
701 | def test_update_issue_should_ignore_bogus_issue_ids_in_subject | |
702 | journal = submit_email('ticket_reply_with_status.eml') do |email| |
|
702 | journal = submit_email('ticket_reply_with_status.eml') do |email| | |
703 | assert email.sub!(/^Subject:.*$/, "Subject: Re: [12345#1][bogus#1][Feature request #2] Add ingredients categories") |
|
703 | assert email.sub!(/^Subject:.*$/, "Subject: Re: [12345#1][bogus#1][Feature request #2] Add ingredients categories") | |
704 | end |
|
704 | end | |
705 | assert journal.is_a?(Journal) |
|
705 | assert journal.is_a?(Journal) | |
706 | assert_equal Issue.find(2), journal.journalized |
|
706 | assert_equal Issue.find(2), journal.journalized | |
707 | end |
|
707 | end | |
708 |
|
708 | |||
709 | def test_update_issue_with_attribute_changes |
|
709 | def test_update_issue_with_attribute_changes | |
710 | # This email contains: 'Status: Resolved' |
|
710 | # This email contains: 'Status: Resolved' | |
711 | journal = submit_email('ticket_reply_with_status.eml') |
|
711 | journal = submit_email('ticket_reply_with_status.eml') | |
712 | assert journal.is_a?(Journal) |
|
712 | assert journal.is_a?(Journal) | |
713 | issue = Issue.find(journal.issue.id) |
|
713 | issue = Issue.find(journal.issue.id) | |
714 | assert_equal User.find_by_login('jsmith'), journal.user |
|
714 | assert_equal User.find_by_login('jsmith'), journal.user | |
715 | assert_equal Issue.find(2), journal.journalized |
|
715 | assert_equal Issue.find(2), journal.journalized | |
716 | assert_match /This is reply/, journal.notes |
|
716 | assert_match /This is reply/, journal.notes | |
717 | assert_equal 'Feature request', journal.issue.tracker.name |
|
717 | assert_equal 'Feature request', journal.issue.tracker.name | |
718 | assert_equal IssueStatus.find_by_name("Resolved"), issue.status |
|
718 | assert_equal IssueStatus.find_by_name("Resolved"), issue.status | |
719 | assert_equal '2010-01-01', issue.start_date.to_s |
|
719 | assert_equal '2010-01-01', issue.start_date.to_s | |
720 | assert_equal '2010-12-31', issue.due_date.to_s |
|
720 | assert_equal '2010-12-31', issue.due_date.to_s | |
721 | assert_equal User.find_by_login('jsmith'), issue.assigned_to |
|
721 | assert_equal User.find_by_login('jsmith'), issue.assigned_to | |
722 | assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value |
|
722 | assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value | |
723 | # keywords should be removed from the email body |
|
723 | # keywords should be removed from the email body | |
724 | assert !journal.notes.match(/^Status:/i) |
|
724 | assert !journal.notes.match(/^Status:/i) | |
725 | assert !journal.notes.match(/^Start Date:/i) |
|
725 | assert !journal.notes.match(/^Start Date:/i) | |
726 | end |
|
726 | end | |
727 |
|
727 | |||
728 | def test_update_issue_with_attachment |
|
728 | def test_update_issue_with_attachment | |
729 | assert_difference 'Journal.count' do |
|
729 | assert_difference 'Journal.count' do | |
730 | assert_difference 'JournalDetail.count' do |
|
730 | assert_difference 'JournalDetail.count' do | |
731 | assert_difference 'Attachment.count' do |
|
731 | assert_difference 'Attachment.count' do | |
732 | assert_no_difference 'Issue.count' do |
|
732 | assert_no_difference 'Issue.count' do | |
733 | journal = submit_email('ticket_with_attachment.eml') do |raw| |
|
733 | journal = submit_email('ticket_with_attachment.eml') do |raw| | |
734 | raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories' |
|
734 | raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories' | |
735 | end |
|
735 | end | |
736 | end |
|
736 | end | |
737 | end |
|
737 | end | |
738 | end |
|
738 | end | |
739 | end |
|
739 | end | |
740 | journal = Journal.order('id DESC').first |
|
740 | journal = Journal.order('id DESC').first | |
741 | assert_equal Issue.find(2), journal.journalized |
|
741 | assert_equal Issue.find(2), journal.journalized | |
742 | assert_equal 1, journal.details.size |
|
742 | assert_equal 1, journal.details.size | |
743 |
|
743 | |||
744 | detail = journal.details.first |
|
744 | detail = journal.details.first | |
745 | assert_equal 'attachment', detail.property |
|
745 | assert_equal 'attachment', detail.property | |
746 | assert_equal 'Paella.jpg', detail.value |
|
746 | assert_equal 'Paella.jpg', detail.value | |
747 | end |
|
747 | end | |
748 |
|
748 | |||
749 | def test_update_issue_should_send_email_notification |
|
749 | def test_update_issue_should_send_email_notification | |
750 | ActionMailer::Base.deliveries.clear |
|
750 | ActionMailer::Base.deliveries.clear | |
751 | journal = submit_email('ticket_reply.eml') |
|
751 | journal = submit_email('ticket_reply.eml') | |
752 | assert journal.is_a?(Journal) |
|
752 | assert journal.is_a?(Journal) | |
753 | assert_equal 1, ActionMailer::Base.deliveries.size |
|
753 | assert_equal 1, ActionMailer::Base.deliveries.size | |
754 | end |
|
754 | end | |
755 |
|
755 | |||
756 | def test_update_issue_should_not_set_defaults |
|
756 | def test_update_issue_should_not_set_defaults | |
757 | journal = submit_email( |
|
757 | journal = submit_email( | |
758 | 'ticket_reply.eml', |
|
758 | 'ticket_reply.eml', | |
759 | :issue => {:tracker => 'Support request', :priority => 'High'} |
|
759 | :issue => {:tracker => 'Support request', :priority => 'High'} | |
760 | ) |
|
760 | ) | |
761 | assert journal.is_a?(Journal) |
|
761 | assert journal.is_a?(Journal) | |
762 | assert_match /This is reply/, journal.notes |
|
762 | assert_match /This is reply/, journal.notes | |
763 | assert_equal 'Feature request', journal.issue.tracker.name |
|
763 | assert_equal 'Feature request', journal.issue.tracker.name | |
764 | assert_equal 'Normal', journal.issue.priority.name |
|
764 | assert_equal 'Normal', journal.issue.priority.name | |
765 | end |
|
765 | end | |
766 |
|
766 | |||
767 | def test_replying_to_a_private_note_should_add_reply_as_private |
|
767 | def test_replying_to_a_private_note_should_add_reply_as_private | |
768 | private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2) |
|
768 | private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2) | |
769 |
|
769 | |||
770 | assert_difference 'Journal.count' do |
|
770 | assert_difference 'Journal.count' do | |
771 | journal = submit_email('ticket_reply.eml') do |email| |
|
771 | journal = submit_email('ticket_reply.eml') do |email| | |
772 | email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>" |
|
772 | email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>" | |
773 | end |
|
773 | end | |
774 |
|
774 | |||
775 | assert_kind_of Journal, journal |
|
775 | assert_kind_of Journal, journal | |
776 | assert_match /This is reply/, journal.notes |
|
776 | assert_match /This is reply/, journal.notes | |
777 | assert_equal true, journal.private_notes |
|
777 | assert_equal true, journal.private_notes | |
778 | end |
|
778 | end | |
779 | end |
|
779 | end | |
780 |
|
780 | |||
781 | def test_reply_to_a_message |
|
781 | def test_reply_to_a_message | |
782 | m = submit_email('message_reply.eml') |
|
782 | m = submit_email('message_reply.eml') | |
783 | assert m.is_a?(Message) |
|
783 | assert m.is_a?(Message) | |
784 | assert !m.new_record? |
|
784 | assert !m.new_record? | |
785 | m.reload |
|
785 | m.reload | |
786 | assert_equal 'Reply via email', m.subject |
|
786 | assert_equal 'Reply via email', m.subject | |
787 | # The email replies to message #2 which is part of the thread of message #1 |
|
787 | # The email replies to message #2 which is part of the thread of message #1 | |
788 | assert_equal Message.find(1), m.parent |
|
788 | assert_equal Message.find(1), m.parent | |
789 | end |
|
789 | end | |
790 |
|
790 | |||
791 | def test_reply_to_a_message_by_subject |
|
791 | def test_reply_to_a_message_by_subject | |
792 | m = submit_email('message_reply_by_subject.eml') |
|
792 | m = submit_email('message_reply_by_subject.eml') | |
793 | assert m.is_a?(Message) |
|
793 | assert m.is_a?(Message) | |
794 | assert !m.new_record? |
|
794 | assert !m.new_record? | |
795 | m.reload |
|
795 | m.reload | |
796 | assert_equal 'Reply to the first post', m.subject |
|
796 | assert_equal 'Reply to the first post', m.subject | |
797 | assert_equal Message.find(1), m.parent |
|
797 | assert_equal Message.find(1), m.parent | |
798 | end |
|
798 | end | |
799 |
|
799 | |||
800 |
def test_should_ |
|
800 | def test_should_convert_tags_of_html_only_emails | |
801 | issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'}) |
|
801 | with_settings :text_formatting => 'textile' do | |
|
802 | issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'}) | |||
|
803 | assert issue.is_a?(Issue) | |||
|
804 | assert !issue.new_record? | |||
|
805 | issue.reload | |||
|
806 | assert_equal 'HTML email', issue.subject | |||
|
807 | assert_equal "This is a *html-only* email.\r\n\r\nh1. With a title\r\n\r\nand a paragraph.", issue.description | |||
|
808 | end | |||
|
809 | end | |||
|
810 | ||||
|
811 | def test_should_handle_outlook_web_access_2010_html_only | |||
|
812 | issue = submit_email('outlook_web_access_2010_html_only.eml', :issue => {:project => 'ecookbook'}) | |||
|
813 | assert issue.is_a?(Issue) | |||
|
814 | issue.reload | |||
|
815 | assert_equal 'Upgrade Redmine to 3.0.x', issue.subject | |||
|
816 | assert_equal "A mess.\r\n\r\n--Geoff Maciolek\r\nMYCOMPANYNAME, LLC", issue.description | |||
|
817 | end | |||
|
818 | ||||
|
819 | def test_should_handle_outlook_2010_html_only | |||
|
820 | issue = submit_email('outlook_2010_html_only.eml', :issue => {:project => 'ecookbook'}) | |||
802 | assert issue.is_a?(Issue) |
|
821 | assert issue.is_a?(Issue) | |
803 | assert !issue.new_record? |
|
|||
804 | issue.reload |
|
822 | issue.reload | |
805 |
assert_equal ' |
|
823 | assert_equal 'Test email', issue.subject | |
806 | assert_equal 'This is a html-only email.', issue.description |
|
824 | assert_equal "Simple, unadorned test email generated by Outlook 2010. It is in HTML format, but" + | |
|
825 | " no special formatting has been chosen. Iβm going to save this as a draft and then manually" + | |||
|
826 | " drop it into the Inbox for scraping by Redmine 3.0.2.", issue.description | |||
807 | end |
|
827 | end | |
808 |
|
828 | |||
809 | test "truncate emails with no setting should add the entire email into the issue" do |
|
829 | test "truncate emails with no setting should add the entire email into the issue" do | |
810 | with_settings :mail_handler_body_delimiters => '' do |
|
830 | with_settings :mail_handler_body_delimiters => '' do | |
811 | issue = submit_email('ticket_on_given_project.eml') |
|
831 | issue = submit_email('ticket_on_given_project.eml') | |
812 | assert_issue_created(issue) |
|
832 | assert_issue_created(issue) | |
813 | assert issue.description.include?('---') |
|
833 | assert issue.description.include?('---') | |
814 | assert issue.description.include?('This paragraph is after the delimiter') |
|
834 | assert issue.description.include?('This paragraph is after the delimiter') | |
815 | end |
|
835 | end | |
816 | end |
|
836 | end | |
817 |
|
837 | |||
818 | test "truncate emails with a single string should truncate the email at the delimiter for the issue" do |
|
838 | test "truncate emails with a single string should truncate the email at the delimiter for the issue" do | |
819 | with_settings :mail_handler_body_delimiters => '---' do |
|
839 | with_settings :mail_handler_body_delimiters => '---' do | |
820 | issue = submit_email('ticket_on_given_project.eml') |
|
840 | issue = submit_email('ticket_on_given_project.eml') | |
821 | assert_issue_created(issue) |
|
841 | assert_issue_created(issue) | |
822 | assert issue.description.include?('This paragraph is before delimiters') |
|
842 | assert issue.description.include?('This paragraph is before delimiters') | |
823 | assert issue.description.include?('--- This line starts with a delimiter') |
|
843 | assert issue.description.include?('--- This line starts with a delimiter') | |
824 | assert !issue.description.match(/^---$/) |
|
844 | assert !issue.description.match(/^---$/) | |
825 | assert !issue.description.include?('This paragraph is after the delimiter') |
|
845 | assert !issue.description.include?('This paragraph is after the delimiter') | |
826 | end |
|
846 | end | |
827 | end |
|
847 | end | |
828 |
|
848 | |||
829 | test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do |
|
849 | test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do | |
830 | with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do |
|
850 | with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do | |
831 | journal = submit_email('issue_update_with_quoted_reply_above.eml') |
|
851 | journal = submit_email('issue_update_with_quoted_reply_above.eml') | |
832 | assert journal.is_a?(Journal) |
|
852 | assert journal.is_a?(Journal) | |
833 | assert journal.notes.include?('An update to the issue by the sender.') |
|
853 | assert journal.notes.include?('An update to the issue by the sender.') | |
834 | assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) |
|
854 | assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) | |
835 | assert !journal.notes.include?('Looks like the JSON api for projects was missed.') |
|
855 | assert !journal.notes.include?('Looks like the JSON api for projects was missed.') | |
836 | end |
|
856 | end | |
837 | end |
|
857 | end | |
838 |
|
858 | |||
839 | test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do |
|
859 | test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do | |
840 | with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do |
|
860 | with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do | |
841 | journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml') |
|
861 | journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml') | |
842 | assert journal.is_a?(Journal) |
|
862 | assert journal.is_a?(Journal) | |
843 | assert journal.notes.include?('An update to the issue by the sender.') |
|
863 | assert journal.notes.include?('An update to the issue by the sender.') | |
844 | assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) |
|
864 | assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---")) | |
845 | assert !journal.notes.include?('Looks like the JSON api for projects was missed.') |
|
865 | assert !journal.notes.include?('Looks like the JSON api for projects was missed.') | |
846 | end |
|
866 | end | |
847 | end |
|
867 | end | |
848 |
|
868 | |||
849 | test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do |
|
869 | test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do | |
850 | with_settings :mail_handler_body_delimiters => "---\nBREAK" do |
|
870 | with_settings :mail_handler_body_delimiters => "---\nBREAK" do | |
851 | issue = submit_email('ticket_on_given_project.eml') |
|
871 | issue = submit_email('ticket_on_given_project.eml') | |
852 | assert_issue_created(issue) |
|
872 | assert_issue_created(issue) | |
853 | assert issue.description.include?('This paragraph is before delimiters') |
|
873 | assert issue.description.include?('This paragraph is before delimiters') | |
854 | assert !issue.description.include?('BREAK') |
|
874 | assert !issue.description.include?('BREAK') | |
855 | assert !issue.description.include?('This paragraph is between delimiters') |
|
875 | assert !issue.description.include?('This paragraph is between delimiters') | |
856 | assert !issue.description.match(/^---$/) |
|
876 | assert !issue.description.match(/^---$/) | |
857 | assert !issue.description.include?('This paragraph is after the delimiter') |
|
877 | assert !issue.description.include?('This paragraph is after the delimiter') | |
858 | end |
|
878 | end | |
859 | end |
|
879 | end | |
860 |
|
880 | |||
861 | def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored |
|
881 | def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored | |
862 | with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do |
|
882 | with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do | |
863 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) |
|
883 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) | |
864 | assert issue.is_a?(Issue) |
|
884 | assert issue.is_a?(Issue) | |
865 | assert !issue.new_record? |
|
885 | assert !issue.new_record? | |
866 | assert_equal 0, issue.reload.attachments.size |
|
886 | assert_equal 0, issue.reload.attachments.size | |
867 | end |
|
887 | end | |
868 | end |
|
888 | end | |
869 |
|
889 | |||
870 | def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached |
|
890 | def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached | |
871 | with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do |
|
891 | with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do | |
872 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) |
|
892 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) | |
873 | assert issue.is_a?(Issue) |
|
893 | assert issue.is_a?(Issue) | |
874 | assert !issue.new_record? |
|
894 | assert !issue.new_record? | |
875 | assert_equal 1, issue.reload.attachments.size |
|
895 | assert_equal 1, issue.reload.attachments.size | |
876 | end |
|
896 | end | |
877 | end |
|
897 | end | |
878 |
|
898 | |||
879 | def test_email_with_long_subject_line |
|
899 | def test_email_with_long_subject_line | |
880 | issue = submit_email('ticket_with_long_subject.eml') |
|
900 | issue = submit_email('ticket_with_long_subject.eml') | |
881 | assert issue.is_a?(Issue) |
|
901 | assert issue.is_a?(Issue) | |
882 | assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255] |
|
902 | assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255] | |
883 | end |
|
903 | end | |
884 |
|
904 | |||
885 | def test_first_keyword_should_be_matched |
|
905 | def test_first_keyword_should_be_matched | |
886 | issue = submit_email('ticket_with_duplicate_keyword.eml', :allow_override => 'priority') |
|
906 | issue = submit_email('ticket_with_duplicate_keyword.eml', :allow_override => 'priority') | |
887 | assert issue.is_a?(Issue) |
|
907 | assert issue.is_a?(Issue) | |
888 | assert_equal 'High', issue.priority.name |
|
908 | assert_equal 'High', issue.priority.name | |
889 | end |
|
909 | end | |
890 |
|
910 | |||
891 | def test_keyword_after_delimiter_should_be_ignored |
|
911 | def test_keyword_after_delimiter_should_be_ignored | |
892 | with_settings :mail_handler_body_delimiters => "== DELIMITER ==" do |
|
912 | with_settings :mail_handler_body_delimiters => "== DELIMITER ==" do | |
893 | issue = submit_email('ticket_with_keyword_after_delimiter.eml', :allow_override => 'priority') |
|
913 | issue = submit_email('ticket_with_keyword_after_delimiter.eml', :allow_override => 'priority') | |
894 | assert issue.is_a?(Issue) |
|
914 | assert issue.is_a?(Issue) | |
895 | assert_equal 'Normal', issue.priority.name |
|
915 | assert_equal 'Normal', issue.priority.name | |
896 | end |
|
916 | end | |
897 | end |
|
917 | end | |
898 |
|
918 | |||
899 | def test_new_user_from_attributes_should_return_valid_user |
|
919 | def test_new_user_from_attributes_should_return_valid_user | |
900 | to_test = { |
|
920 | to_test = { | |
901 | # [address, name] => [login, firstname, lastname] |
|
921 | # [address, name] => [login, firstname, lastname] | |
902 | ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'], |
|
922 | ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'], | |
903 | ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'], |
|
923 | ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'], | |
904 | ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'], |
|
924 | ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'], | |
905 | ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'], |
|
925 | ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'], | |
906 | ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'], |
|
926 | ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'], | |
907 | ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh'] |
|
927 | ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh'] | |
908 | } |
|
928 | } | |
909 |
|
929 | |||
910 | to_test.each do |attrs, expected| |
|
930 | to_test.each do |attrs, expected| | |
911 | user = MailHandler.new_user_from_attributes(attrs.first, attrs.last) |
|
931 | user = MailHandler.new_user_from_attributes(attrs.first, attrs.last) | |
912 |
|
932 | |||
913 | assert user.valid?, user.errors.full_messages.to_s |
|
933 | assert user.valid?, user.errors.full_messages.to_s | |
914 | assert_equal attrs.first, user.mail |
|
934 | assert_equal attrs.first, user.mail | |
915 | assert_equal expected[0], user.login |
|
935 | assert_equal expected[0], user.login | |
916 | assert_equal expected[1], user.firstname |
|
936 | assert_equal expected[1], user.firstname | |
917 | assert_equal expected[2], user.lastname |
|
937 | assert_equal expected[2], user.lastname | |
918 | assert_equal 'only_my_events', user.mail_notification |
|
938 | assert_equal 'only_my_events', user.mail_notification | |
919 | end |
|
939 | end | |
920 | end |
|
940 | end | |
921 |
|
941 | |||
922 | def test_new_user_from_attributes_should_use_default_login_if_invalid |
|
942 | def test_new_user_from_attributes_should_use_default_login_if_invalid | |
923 | user = MailHandler.new_user_from_attributes('foo+bar@example.net') |
|
943 | user = MailHandler.new_user_from_attributes('foo+bar@example.net') | |
924 | assert user.valid? |
|
944 | assert user.valid? | |
925 | assert user.login =~ /^user[a-f0-9]+$/ |
|
945 | assert user.login =~ /^user[a-f0-9]+$/ | |
926 | assert_equal 'foo+bar@example.net', user.mail |
|
946 | assert_equal 'foo+bar@example.net', user.mail | |
927 | end |
|
947 | end | |
928 |
|
948 | |||
929 | def test_new_user_with_utf8_encoded_fullname_should_be_decoded |
|
949 | def test_new_user_with_utf8_encoded_fullname_should_be_decoded | |
930 | assert_difference 'User.count' do |
|
950 | assert_difference 'User.count' do | |
931 | issue = submit_email( |
|
951 | issue = submit_email( | |
932 | 'fullname_of_sender_as_utf8_encoded.eml', |
|
952 | 'fullname_of_sender_as_utf8_encoded.eml', | |
933 | :issue => {:project => 'ecookbook'}, |
|
953 | :issue => {:project => 'ecookbook'}, | |
934 | :unknown_user => 'create' |
|
954 | :unknown_user => 'create' | |
935 | ) |
|
955 | ) | |
936 | end |
|
956 | end | |
937 | user = User.order('id DESC').first |
|
957 | user = User.order('id DESC').first | |
938 | assert_equal "foo@example.org", user.mail |
|
958 | assert_equal "foo@example.org", user.mail | |
939 | str1 = "\xc3\x84\xc3\xa4".force_encoding('UTF-8') |
|
959 | str1 = "\xc3\x84\xc3\xa4".force_encoding('UTF-8') | |
940 | str2 = "\xc3\x96\xc3\xb6".force_encoding('UTF-8') |
|
960 | str2 = "\xc3\x96\xc3\xb6".force_encoding('UTF-8') | |
941 | assert_equal str1, user.firstname |
|
961 | assert_equal str1, user.firstname | |
942 | assert_equal str2, user.lastname |
|
962 | assert_equal str2, user.lastname | |
943 | end |
|
963 | end | |
944 |
|
964 | |||
945 | def test_extract_options_from_env_should_return_options |
|
965 | def test_extract_options_from_env_should_return_options | |
946 | options = MailHandler.extract_options_from_env({ |
|
966 | options = MailHandler.extract_options_from_env({ | |
947 | 'tracker' => 'defect', |
|
967 | 'tracker' => 'defect', | |
948 | 'project' => 'foo', |
|
968 | 'project' => 'foo', | |
949 | 'unknown_user' => 'create' |
|
969 | 'unknown_user' => 'create' | |
950 | }) |
|
970 | }) | |
951 |
|
971 | |||
952 | assert_equal({ |
|
972 | assert_equal({ | |
953 | :issue => {:tracker => 'defect', :project => 'foo'}, |
|
973 | :issue => {:tracker => 'defect', :project => 'foo'}, | |
954 | :unknown_user => 'create' |
|
974 | :unknown_user => 'create' | |
955 | }, options) |
|
975 | }, options) | |
956 | end |
|
976 | end | |
957 |
|
977 | |||
958 | def test_safe_receive_should_rescue_exceptions_and_return_false |
|
978 | def test_safe_receive_should_rescue_exceptions_and_return_false | |
959 | MailHandler.stubs(:receive).raises(Exception.new "Something went wrong") |
|
979 | MailHandler.stubs(:receive).raises(Exception.new "Something went wrong") | |
960 |
|
980 | |||
961 | assert_equal false, MailHandler.safe_receive |
|
981 | assert_equal false, MailHandler.safe_receive | |
962 | end |
|
982 | end | |
963 |
|
983 | |||
964 | private |
|
984 | private | |
965 |
|
985 | |||
966 | def submit_email(filename, options={}) |
|
986 | def submit_email(filename, options={}) | |
967 | raw = IO.read(File.join(FIXTURES_PATH, filename)) |
|
987 | raw = IO.read(File.join(FIXTURES_PATH, filename)) | |
968 | yield raw if block_given? |
|
988 | yield raw if block_given? | |
969 | MailHandler.receive(raw, options) |
|
989 | MailHandler.receive(raw, options) | |
970 | end |
|
990 | end | |
971 |
|
991 | |||
972 | def assert_issue_created(issue) |
|
992 | def assert_issue_created(issue) | |
973 | assert issue.is_a?(Issue) |
|
993 | assert issue.is_a?(Issue) | |
974 | assert !issue.new_record? |
|
994 | assert !issue.new_record? | |
975 | issue.reload |
|
995 | issue.reload | |
976 | end |
|
996 | end | |
977 | end |
|
997 | end |
General Comments 0
You need to be logged in to leave comments.
Login now