Merge lp:~therp-nl/server-env-tools/6.1-fetchmail_attach_from_folder into lp:~server-env-tools-core-editors/server-env-tools/6.1

Proposed by Holger Brunn (Therp)
Status: Merged
Merged at revision: 35
Proposed branch: lp:~therp-nl/server-env-tools/6.1-fetchmail_attach_from_folder
Merge into: lp:~server-env-tools-core-editors/server-env-tools/6.1
Diff against target: 1012 lines (+938/-0)
14 files modified
fetchmail_attach_from_folder/__init__.py (+25/-0)
fetchmail_attach_from_folder/__openerp__.py (+45/-0)
fetchmail_attach_from_folder/match_algorithm/__init__.py (+26/-0)
fetchmail_attach_from_folder/match_algorithm/base.py (+43/-0)
fetchmail_attach_from_folder/match_algorithm/email_domain.py (+44/-0)
fetchmail_attach_from_folder/match_algorithm/email_exact.py (+56/-0)
fetchmail_attach_from_folder/match_algorithm/openerp_standard.py (+51/-0)
fetchmail_attach_from_folder/model/__init__.py (+24/-0)
fetchmail_attach_from_folder/model/fetchmail_server.py (+287/-0)
fetchmail_attach_from_folder/model/fetchmail_server_folder.py (+120/-0)
fetchmail_attach_from_folder/view/fetchmail_server.xml (+58/-0)
fetchmail_attach_from_folder/wizard/__init__.py (+23/-0)
fetchmail_attach_from_folder/wizard/attach_mail_manually.py (+110/-0)
fetchmail_attach_from_folder/wizard/attach_mail_manually.xml (+26/-0)
To merge this branch: bzr merge lp:~therp-nl/server-env-tools/6.1-fetchmail_attach_from_folder
Reviewer Review Type Date Requested Status
Alexandre Fayolle - camptocamp code review, no test Approve
Ronald Portier (Therp) (community) Approve
Stefan Rijnhart (Opener) Pending
Review via email: mp+159651@code.launchpad.net

This proposal supersedes a proposal from 2013-04-05.

Description of the change

Found a concurrency problem, causing long running fetching threads to be called multiple times, resulting in duplicated mails

To post a comment you must log in.
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote : Posted in a previous version of this proposal
review: Approve
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote : Posted in a previous version of this proposal

Suggested enhancements:

formatting : would have been nice to get the proper spaces added (after commas, before and after assignment equal signs...)

the fetch_mail method (line 416 of the MP) could be split. I would personnally add one method for the body of the "for folder" loop and another for the body of the for message loop. This gains 8 columns, with allows for less line wrapping and clearer code, within the 80 col convention.

line 497: prefer "key in dictionary" to dictionary.has_key(key).

line 632: you are passing a cmp func to sorted to compare by the first element of a tuple. There are several small issues in there:

1. it is more efficient to use the key named argument (passing itemgetter(0) as key function in this case)
2. but it looks to me that the default sorting of Python does exactly what you want
3. you have a local list : using tuple(sorted(algorithms)) eats up unnecessary memory. Just use algorithms.sort() then return algorithms

in the column definitions (line 634 of the MP and following), using double quotes as delimiter removes the need to escape the single quotes inside the various strings, which makes the code easier to read.

review: Needs Fixing (code review, no test)
Revision history for this message
Ronald Portier (Therp) (rportier1962) wrote : Posted in a previous version of this proposal

Hello Holger,

I made a slightly modified branch:
https://code.launchpad.net/~therp-nl/server-env-tools/6.1-fetchmail_attach_mail_from_folder_rpo

- solved a problem when trying to match on either, from, or to, or cc, or bcc: code was chocking on fields not present in mail.
- enhanced change of matching by converting address to lowercase. To complement this, really should be a good idea to have only lower case e-mail adresses in for instance res.partner.address as well. Or have a view that always converts to lowercase...
- some minor code reorganisations to make it easer to step debug and see what is going on.

Ronald

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote : Posted in a previous version of this proposal

Thanks Ronald, would you issue a merge request on this branch as a reminder? I see that assumingly your IDE already took care of a few formatting issues that were pointed out by Alexandre, but there's more to do for me

Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote : Posted in a previous version of this proposal

Thanks everyone, great to see such collaboration!

review: Approve
Revision history for this message
Ronald Portier (Therp) (rportier1962) wrote : Posted in a previous version of this proposal

Holger,

I just noticed that any mail retrieved gets a status 'received'.

However, it is possible to retrieve both received and send-mail.

So if the customer-email address is found in a sender field (most likely from), the mail is really received.

If it found in a receiver field (to, cc or bcc), the mail should get a status sent.

Kind regard, Ronald

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote : Posted in a previous version of this proposal

Ronald: That's a very good point, thanks. But I think it depends too much on the configured object/field what the state of a message should be to put it into the code. That's why I made it a configuration option for a folder in rev30, with 'received' as default.

Alexandre: Today I did the refactoring you proposed, now I think all your points are covered. Could you have another look?

Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote : Posted in a previous version of this proposal

LGTM.

Thanks for taking the time to implement my suggestions.

review: Approve (code review, no test)
Revision history for this message
Ronald Portier (Therp) (rportier1962) wrote : Posted in a previous version of this proposal

Holger,

Great improvements! Thanx.

Sorry to bother you, but I have one more concern: when we are searching for matches, I think there will be a danger that we match on our own e-mail, instead of the partner e-mail.

Example, suppose I have a folder for all my customer mail. Matching will be done on email in res.partner.address.

As I have ingoing and outgoing mail in the same folder, I create two configurations/folders. One having to, cc and bcc as Field (email), and linking it to status send. The other to from and linking it to status received.

Now when I send a mail to a customer, it will find my own e-mail in from, an ingoing mail will be registered, with myself as the customer.

So I think there should be a way to exclude our own e-mail addresses from the search. Maybe by having all mail addresses with our own domain (might be multiple!) excluded from the search results.

What do you think?

Regards, Ronald

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote : Posted in a previous version of this proposal

I think you can exclude such cases by setting a domain in your configuration. So for matching customers only, you could use [('is_customer','=',True)], causing only customers to be possible candidates for matching in the first place. Or filter out employees: [('is_employee','=',False)]. The domain expression is passed directly to the model's search function, see currently lines 261ff in the MP.

In the end you get an expression like
((model_field=='address1') || (model_field=='address2') || ...) && your_domain_expression

Does this help?

Revision history for this message
Holger Brunn (Therp) (hbrunn) wrote : Posted in a previous version of this proposal

now I realize btw that today's refactoring would be a lot more useful if the functions passed down the ids of the mail.message(s) created. I'll fix that tomorrow

Revision history for this message
Ronald Portier (Therp) (rportier1962) wrote : Posted in a previous version of this proposal

Approve.

I tested the following use-case: single imap folder contains mail sent to and received from customers.

Did the following configuration;
- Created an incoming server document, with all the usual options
- Linked two folder configurations to this document:

1. - Folder 'Klanten' (customers)
   - match algorithm: Exact mailaddress
   - Model 'relatieadressen' (res.partner.address)
   - Field (emal): from
   - Field (model): email
   - Message state: Received
   - Domain: [('partner_id.customer', '=', True)]
   - Delete matches: False
   - Flag nonmatching: True
   - Use first match: True
2. The same as 1., except for the following:
   - Field (email): to, cc, bcc
   - Message state: Sent

I then used my mail client to send a mail wit an attachment (here our offer....) to a test customer-email. Then I used the test customer email account to sent back a reply, (I agree to the offer, find enclosed...), with another attachment.

Then I placed both the e-mail sent, and the reply received in the folder Klanten (customers) and ran the receive mail action in the incoming mail server document.

Outcome was as expected:
- I could find both mails, with the right status, on the tab 'Email' within the partner record
- I could see and open both the outgoing and the incoming attachment from within OpenERP.

Kind regards,

Ronald

review: Approve
Revision history for this message
Ronald Portier (Therp) (rportier1962) wrote :

Saw the problem. Solution looks good to me.

review: Approve
34. By Ronald Portier (Therp)

[MRG] [ADD] mail_client_view: gives normal users access to the mail views.

The only other possibilities users have of looking at e-mails, is through the
partner object, or other objects mails are directly linked to. This makes it
impossible to get an overall view, or to search through mails.

Furthermore the module makes newly received mails stand out with a red circle
in the mail view. These mails are in need of action. Via a simple click a user
can either mark a message as also in need of action, or the other way around,
as having been handled.

Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) :
review: Approve (code review, no test)
35. By Holger Brunn (Therp)

[MRG][ADD] fetchmail_attach_from_folder

Adds the possibility to attach emails from a certain IMAP folder to objects,
ie partners. Matching is done via several algorithms, ie email address.

This gives a simple possibility to archive emails in OpenERP without a mail
client integration.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'fetchmail_attach_from_folder'
2=== added file 'fetchmail_attach_from_folder/__init__.py'
3--- fetchmail_attach_from_folder/__init__.py 1970-01-01 00:00:00 +0000
4+++ fetchmail_attach_from_folder/__init__.py 2013-04-18 14:54:31 +0000
5@@ -0,0 +1,25 @@
6+# -*- encoding: utf-8 -*-
7+##############################################################################
8+#
9+# OpenERP, Open Source Management Solution
10+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
11+# All Rights Reserved
12+#
13+# This program is free software: you can redistribute it and/or modify
14+# it under the terms of the GNU Affero General Public License as
15+# published by the Free Software Foundation, either version 3 of the
16+# License, or (at your option) any later version.
17+#
18+# This program is distributed in the hope that it will be useful,
19+# but WITHOUT ANY WARRANTY; without even the implied warranty of
20+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21+# GNU Affero General Public License for more details.
22+#
23+# You should have received a copy of the GNU Affero General Public License
24+# along with this program. If not, see <http://www.gnu.org/licenses/>.
25+#
26+##############################################################################
27+
28+import match_algorithm
29+import model
30+import wizard
31
32=== added file 'fetchmail_attach_from_folder/__openerp__.py'
33--- fetchmail_attach_from_folder/__openerp__.py 1970-01-01 00:00:00 +0000
34+++ fetchmail_attach_from_folder/__openerp__.py 2013-04-18 14:54:31 +0000
35@@ -0,0 +1,45 @@
36+# -*- encoding: utf-8 -*-
37+##############################################################################
38+#
39+# OpenERP, Open Source Management Solution
40+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
41+# All Rights Reserved
42+#
43+# This program is free software: you can redistribute it and/or modify
44+# it under the terms of the GNU Affero General Public License as
45+# published by the Free Software Foundation, either version 3 of the
46+# License, or (at your option) any later version.
47+#
48+# This program is distributed in the hope that it will be useful,
49+# but WITHOUT ANY WARRANTY; without even the implied warranty of
50+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
51+# GNU Affero General Public License for more details.
52+#
53+# You should have received a copy of the GNU Affero General Public License
54+# along with this program. If not, see <http://www.gnu.org/licenses/>.
55+#
56+##############################################################################
57+
58+{
59+ 'name': 'Attach mails in an IMAP folder to existing objects',
60+ 'version': '1.0',
61+ 'description': """
62+ Adds the possibility to attach emails from a certain IMAP folder to objects,
63+ ie partners. Matching is done via several algorithms, ie email address.
64+
65+ This gives a simple possibility to archive emails in OpenERP without a mail
66+ client integration.
67+ """,
68+ 'author': 'Therp BV',
69+ 'website': 'http://www.therp.nl',
70+ "category": "Tools",
71+ "depends": ['fetchmail'],
72+ 'data': [
73+ 'view/fetchmail_server.xml',
74+ 'wizard/attach_mail_manually.xml',
75+ ],
76+ 'js': [],
77+ 'installable': True,
78+ 'active': False,
79+ 'certificate': '',
80+}
81
82=== added directory 'fetchmail_attach_from_folder/match_algorithm'
83=== added file 'fetchmail_attach_from_folder/match_algorithm/__init__.py'
84--- fetchmail_attach_from_folder/match_algorithm/__init__.py 1970-01-01 00:00:00 +0000
85+++ fetchmail_attach_from_folder/match_algorithm/__init__.py 2013-04-18 14:54:31 +0000
86@@ -0,0 +1,26 @@
87+# -*- encoding: utf-8 -*-
88+##############################################################################
89+#
90+# OpenERP, Open Source Management Solution
91+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
92+# All Rights Reserved
93+#
94+# This program is free software: you can redistribute it and/or modify
95+# it under the terms of the GNU Affero General Public License as
96+# published by the Free Software Foundation, either version 3 of the
97+# License, or (at your option) any later version.
98+#
99+# This program is distributed in the hope that it will be useful,
100+# but WITHOUT ANY WARRANTY; without even the implied warranty of
101+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
102+# GNU Affero General Public License for more details.
103+#
104+# You should have received a copy of the GNU Affero General Public License
105+# along with this program. If not, see <http://www.gnu.org/licenses/>.
106+#
107+##############################################################################
108+
109+import base
110+import email_exact
111+import email_domain
112+import openerp_standard
113
114=== added file 'fetchmail_attach_from_folder/match_algorithm/base.py'
115--- fetchmail_attach_from_folder/match_algorithm/base.py 1970-01-01 00:00:00 +0000
116+++ fetchmail_attach_from_folder/match_algorithm/base.py 2013-04-18 14:54:31 +0000
117@@ -0,0 +1,43 @@
118+# -*- encoding: utf-8 -*-
119+##############################################################################
120+#
121+# OpenERP, Open Source Management Solution
122+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
123+# All Rights Reserved
124+#
125+# This program is free software: you can redistribute it and/or modify
126+# it under the terms of the GNU Affero General Public License as
127+# published by the Free Software Foundation, either version 3 of the
128+# License, or (at your option) any later version.
129+#
130+# This program is distributed in the hope that it will be useful,
131+# but WITHOUT ANY WARRANTY; without even the implied warranty of
132+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
133+# GNU Affero General Public License for more details.
134+#
135+# You should have received a copy of the GNU Affero General Public License
136+# along with this program. If not, see <http://www.gnu.org/licenses/>.
137+#
138+##############################################################################
139+
140+class base(object):
141+ name = None
142+ '''Name shown to the user'''
143+
144+ required_fields = []
145+ '''Fields on fetchmail_server folder that are required for this algorithm'''
146+
147+ readonly_fields = []
148+ '''Fields on fetchmail_server folder that are readonly for this algorithm'''
149+
150+
151+ def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
152+ '''Returns ids found for model with mail_message'''
153+ return []
154+
155+ def handle_match(
156+ self, cr, uid, connection, object_id, folder,
157+ mail_message, mail_message_org, msgid, context=None):
158+ '''Do whatever it takes to handle a match'''
159+ return folder.server_id.attach_mail(connection, object_id, folder,
160+ mail_message, msgid)
161
162=== added file 'fetchmail_attach_from_folder/match_algorithm/email_domain.py'
163--- fetchmail_attach_from_folder/match_algorithm/email_domain.py 1970-01-01 00:00:00 +0000
164+++ fetchmail_attach_from_folder/match_algorithm/email_domain.py 2013-04-18 14:54:31 +0000
165@@ -0,0 +1,44 @@
166+# -*- encoding: utf-8 -*-
167+##############################################################################
168+#
169+# OpenERP, Open Source Management Solution
170+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
171+# All Rights Reserved
172+#
173+# This program is free software: you can redistribute it and/or modify
174+# it under the terms of the GNU Affero General Public License as
175+# published by the Free Software Foundation, either version 3 of the
176+# License, or (at your option) any later version.
177+#
178+# This program is distributed in the hope that it will be useful,
179+# but WITHOUT ANY WARRANTY; without even the implied warranty of
180+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
181+# GNU Affero General Public License for more details.
182+#
183+# You should have received a copy of the GNU Affero General Public License
184+# along with this program. If not, see <http://www.gnu.org/licenses/>.
185+#
186+##############################################################################
187+
188+from email_exact import email_exact
189+
190+class email_domain(email_exact):
191+ '''Search objects by domain name of email address.
192+ Beware of match_first here, this is most likely to get it wrong (gmail)'''
193+ name = 'Domain of email address'
194+
195+ def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
196+ ids = super(email_domain, self).search_matches(
197+ cr, uid, conf, mail_message, mail_message_org)
198+ if not ids:
199+ domains = []
200+ for addr in self._get_mailaddresses(conf, mail_message):
201+ domains.append(addr.split('@')[-1])
202+ ids = conf.pool.get(conf.model_id.model).search(
203+ cr, uid,
204+ self._get_mailaddress_search_domain(
205+ conf, mail_message,
206+ operator='like',
207+ values=['%@'+domain for domain in set(domains)]),
208+ order=conf.model_order)
209+ return ids
210
211=== added file 'fetchmail_attach_from_folder/match_algorithm/email_exact.py'
212--- fetchmail_attach_from_folder/match_algorithm/email_exact.py 1970-01-01 00:00:00 +0000
213+++ fetchmail_attach_from_folder/match_algorithm/email_exact.py 2013-04-18 14:54:31 +0000
214@@ -0,0 +1,56 @@
215+# -*- encoding: utf-8 -*-
216+##############################################################################
217+#
218+# OpenERP, Open Source Management Solution
219+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
220+# All Rights Reserved
221+#
222+# This program is free software: you can redistribute it and/or modify
223+# it under the terms of the GNU Affero General Public License as
224+# published by the Free Software Foundation, either version 3 of the
225+# License, or (at your option) any later version.
226+#
227+# This program is distributed in the hope that it will be useful,
228+# but WITHOUT ANY WARRANTY; without even the implied warranty of
229+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
230+# GNU Affero General Public License for more details.
231+#
232+# You should have received a copy of the GNU Affero General Public License
233+# along with this program. If not, see <http://www.gnu.org/licenses/>.
234+#
235+##############################################################################
236+
237+from base import base
238+from openerp.tools.safe_eval import safe_eval
239+from openerp.addons.mail.mail_message import to_email
240+
241+class email_exact(base):
242+ '''Search for exactly the mailadress as noted in the email'''
243+
244+ name = 'Exact mailadress'
245+ required_fields = ['model_field', 'mail_field']
246+
247+ def _get_mailaddresses(self, conf, mail_message):
248+ mailaddresses = []
249+ fields = conf.mail_field.split(',')
250+ for field in fields:
251+ if field in mail_message:
252+ mailaddresses += to_email(mail_message[field])
253+ return [ addr.lower() for addr in mailaddresses ]
254+
255+ def _get_mailaddress_search_domain(
256+ self, conf, mail_message, operator='=', values=None):
257+ mailaddresses = values or self._get_mailaddresses(
258+ conf, mail_message)
259+ if not mailaddresses:
260+ return [(0, '=', 1)]
261+ search_domain = ((['|'] * (len(mailaddresses) - 1)) + [
262+ (conf.model_field, operator, addr) for addr in mailaddresses] +
263+ safe_eval(conf.domain or '[]'))
264+ return search_domain
265+
266+ def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
267+ conf_model = conf.pool.get(conf.model_id.model)
268+ search_domain = self._get_mailaddress_search_domain(conf, mail_message)
269+ return conf_model.search(
270+ cr, uid, search_domain, order=conf.model_order)
271
272=== added file 'fetchmail_attach_from_folder/match_algorithm/openerp_standard.py'
273--- fetchmail_attach_from_folder/match_algorithm/openerp_standard.py 1970-01-01 00:00:00 +0000
274+++ fetchmail_attach_from_folder/match_algorithm/openerp_standard.py 2013-04-18 14:54:31 +0000
275@@ -0,0 +1,51 @@
276+# -*- encoding: utf-8 -*-
277+##############################################################################
278+#
279+# OpenERP, Open Source Management Solution
280+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
281+# All Rights Reserved
282+#
283+# This program is free software: you can redistribute it and/or modify
284+# it under the terms of the GNU Affero General Public License as
285+# published by the Free Software Foundation, either version 3 of the
286+# License, or (at your option) any later version.
287+#
288+# This program is distributed in the hope that it will be useful,
289+# but WITHOUT ANY WARRANTY; without even the implied warranty of
290+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
291+# GNU Affero General Public License for more details.
292+#
293+# You should have received a copy of the GNU Affero General Public License
294+# along with this program. If not, see <http://www.gnu.org/licenses/>.
295+#
296+##############################################################################
297+
298+from base import base
299+from openerp.tools.safe_eval import safe_eval
300+
301+class openerp_standard(base):
302+ '''No search at all. Use OpenERP's standard mechanism to attach mails to
303+ mail.thread objects. Note that this algorithm always matches.'''
304+
305+ name = 'OpenERP standard'
306+ readonly_fields = ['model_field', 'mail_field', 'match_first', 'domain',
307+ 'model_order', 'flag_nonmatching']
308+
309+ def search_matches(self, cr, uid, conf, mail_message, mail_message_org):
310+ '''Always match. Duplicates will be fished out by message_id'''
311+ return [True]
312+
313+ def handle_match(
314+ self, cr, uid, connection, object_id, folder,
315+ mail_message, mail_message_org, msgid, context):
316+ result = folder.pool.get('mail.thread').message_process(
317+ cr, uid,
318+ folder.model_id.model, mail_message_org,
319+ save_original=folder.server_id.original,
320+ strip_attachments=(not folder.server_id.attach),
321+ context=context)
322+
323+ if folder.delete_matching:
324+ connection.store(msgid, '+FLAGS', '\\DELETED')
325+
326+ return [result]
327
328=== added directory 'fetchmail_attach_from_folder/model'
329=== added file 'fetchmail_attach_from_folder/model/__init__.py'
330--- fetchmail_attach_from_folder/model/__init__.py 1970-01-01 00:00:00 +0000
331+++ fetchmail_attach_from_folder/model/__init__.py 2013-04-18 14:54:31 +0000
332@@ -0,0 +1,24 @@
333+# -*- encoding: utf-8 -*-
334+##############################################################################
335+#
336+# OpenERP, Open Source Management Solution
337+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
338+# All Rights Reserved
339+#
340+# This program is free software: you can redistribute it and/or modify
341+# it under the terms of the GNU Affero General Public License as
342+# published by the Free Software Foundation, either version 3 of the
343+# License, or (at your option) any later version.
344+#
345+# This program is distributed in the hope that it will be useful,
346+# but WITHOUT ANY WARRANTY; without even the implied warranty of
347+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
348+# GNU Affero General Public License for more details.
349+#
350+# You should have received a copy of the GNU Affero General Public License
351+# along with this program. If not, see <http://www.gnu.org/licenses/>.
352+#
353+##############################################################################
354+
355+import fetchmail_server
356+import fetchmail_server_folder
357
358=== added file 'fetchmail_attach_from_folder/model/fetchmail_server.py'
359--- fetchmail_attach_from_folder/model/fetchmail_server.py 1970-01-01 00:00:00 +0000
360+++ fetchmail_attach_from_folder/model/fetchmail_server.py 2013-04-18 14:54:31 +0000
361@@ -0,0 +1,287 @@
362+# -*- encoding: utf-8 -*-
363+##############################################################################
364+#
365+# OpenERP, Open Source Management Solution
366+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
367+# All Rights Reserved
368+#
369+# This program is free software: you can redistribute it and/or modify
370+# it under the terms of the GNU Affero General Public License as
371+# published by the Free Software Foundation, either version 3 of the
372+# License, or (at your option) any later version.
373+#
374+# This program is distributed in the hope that it will be useful,
375+# but WITHOUT ANY WARRANTY; without even the implied warranty of
376+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
377+# GNU Affero General Public License for more details.
378+#
379+# You should have received a copy of the GNU Affero General Public License
380+# along with this program. If not, see <http://www.gnu.org/licenses/>.
381+#
382+##############################################################################
383+
384+import base64
385+import simplejson
386+from lxml import etree
387+from openerp.osv.orm import Model, except_orm, browse_null
388+from openerp.tools.translate import _
389+from openerp.osv import fields
390+from openerp.addons.fetchmail.fetchmail import logger
391+from openerp.tools.misc import UnquoteEvalContext
392+from openerp.tools.safe_eval import safe_eval
393+
394+
395+class fetchmail_server(Model):
396+ _inherit = 'fetchmail.server'
397+
398+ _columns = {
399+ 'folder_ids': fields.one2many(
400+ 'fetchmail.server.folder', 'server_id', 'Folders'),
401+ }
402+
403+ _defaults = {
404+ 'type': 'imap',
405+ }
406+
407+ def __init__(self, pool, cr):
408+ self._columns['object_id'].required = False
409+ return super(fetchmail_server, self).__init__(pool, cr)
410+
411+ def onchange_server_type(
412+ self, cr, uid, ids, server_type=False, ssl=False,
413+ object_id=False):
414+ retval = super(
415+ fetchmail_server, self).onchange_server_type(cr, uid,
416+ ids, server_type, ssl,
417+ object_id)
418+ retval['value']['state'] = 'draft'
419+ return retval
420+
421+ def fetch_mail(self, cr, uid, ids, context=None):
422+ if context is None:
423+ context = {}
424+
425+ check_original = []
426+
427+ for this in self.browse(cr, uid, ids, context):
428+ if this.object_id:
429+ check_original.append(this.id)
430+
431+ context.update(
432+ {
433+ 'fetchmail_server_id': this.id,
434+ 'server_type': this.type
435+ })
436+
437+ connection = this.connect()
438+ for folder in this.folder_ids:
439+ this.handle_folder(connection, folder)
440+
441+ connection.close()
442+
443+ return super(fetchmail_server, self).fetch_mail(
444+ cr, uid, check_original, context)
445+
446+ def handle_folder(self, cr, uid, ids, connection, folder, context=None):
447+ '''Return ids of objects matched'''
448+
449+ matched_object_ids = []
450+
451+ for this in self.browse(cr, uid, ids, context=context):
452+ logger.info('start checking for emails in %s server %s',
453+ folder.path, this.name)
454+
455+ match_algorithm = folder.get_algorithm()
456+
457+ if connection.select(folder.path)[0] != 'OK':
458+ logger.error(
459+ 'Could not open mailbox %s on %s' % (
460+ folder.path, this.server))
461+ connection.select()
462+ continue
463+ result, msgids = this.get_msgids(connection)
464+ if result != 'OK':
465+ logger.error(
466+ 'Could not search mailbox %s on %s' % (
467+ folder.path, this.server))
468+ continue
469+
470+ for msgid in msgids[0].split():
471+ matched_object_ids += this.apply_matching(
472+ connection, folder, msgid, match_algorithm)
473+
474+ logger.info('finished checking for emails in %s server %s',
475+ folder.path, this.name)
476+
477+ return matched_object_ids
478+
479+ def get_msgids(self, cr, uid, ids, connection, context=None):
480+ '''Return imap ids of messages to process'''
481+ return connection.search(None, 'UNDELETED')
482+
483+ def apply_matching(self, cr, uid, ids, connection, folder, msgid,
484+ match_algorithm, context=None):
485+ '''Return ids of objects matched'''
486+
487+ matched_object_ids = []
488+
489+ for this in self.browse(cr, uid, ids, context=context):
490+ result, msgdata = connection.fetch(msgid, '(RFC822)')
491+
492+ if result != 'OK':
493+ logger.error(
494+ 'Could not fetch %s in %s on %s' % (
495+ msgid, folder.path, this.server))
496+ continue
497+
498+ mail_message = self.pool.get('mail.message').parse_message(
499+ msgdata[0][1], this.original)
500+
501+ if self.pool.get('mail.message').search(cr, uid, [
502+ ('message_id', '=', mail_message['message-id'])]):
503+ continue
504+
505+ found_ids = match_algorithm.search_matches(
506+ cr, uid, folder,
507+ mail_message, msgdata[0][1])
508+
509+ if found_ids and (len(found_ids) == 1 or
510+ folder.match_first):
511+ try:
512+ cr.execute('savepoint apply_matching')
513+ match_algorithm.handle_match(
514+ cr, uid, connection,
515+ found_ids[0], folder, mail_message,
516+ msgdata[0][1], msgid, context)
517+ cr.execute('release savepoint apply_matching')
518+ matched_object_ids += found_ids[:1]
519+ except Exception, e:
520+ cr.execute('rollback to savepoint apply_matching')
521+ logger.exception(
522+ "Failed to fetch mail %s from %s",
523+ msgid, this.name)
524+ elif folder.flag_nonmatching:
525+ connection.store(msgid, '+FLAGS', '\\FLAGGED')
526+
527+ return matched_object_ids
528+
529+ def attach_mail(
530+ self, cr, uid, ids, connection, object_id, folder,
531+ mail_message, msgid, context=None):
532+ '''Return ids of messages created'''
533+
534+ mail_message_ids = []
535+
536+ for this in self.browse(cr, uid, ids, context):
537+ partner_id = None
538+ if folder.model_id.model == 'res.partner':
539+ partner_id = object_id
540+ if 'partner_id' in self.pool.get(folder.model_id.model)._columns:
541+ partner_id = self.pool.get(
542+ folder.model_id.model).browse(
543+ cr, uid, object_id, context
544+ ).partner_id.id
545+
546+ attachments=[]
547+ if this.attach and mail_message.get('attachments'):
548+ for attachment in mail_message['attachments']:
549+ fname, fcontent = attachment
550+ if isinstance(fcontent, unicode):
551+ fcontent = fcontent.encode('utf-8')
552+ data_attach = {
553+ 'name': fname,
554+ 'datas': base64.b64encode(str(fcontent)),
555+ 'datas_fname': fname,
556+ 'description': _('Mail attachment'),
557+ 'res_model': folder.model_id.model,
558+ 'res_id': object_id,
559+ }
560+ attachments.append(
561+ self.pool.get('ir.attachment').create(
562+ cr, uid, data_attach, context=context))
563+
564+ mail_message_ids.append(
565+ self.pool.get('mail.message').create(
566+ cr, uid,
567+ {
568+ 'partner_id': partner_id,
569+ 'model': folder.model_id.model,
570+ 'res_id': object_id,
571+ 'body_text': mail_message.get('body'),
572+ 'body_html': mail_message.get('body_html'),
573+ 'subject': mail_message.get('subject'),
574+ 'email_to': mail_message.get('to'),
575+ 'email_from': mail_message.get('from'),
576+ 'email_cc': mail_message.get('cc'),
577+ 'reply_to': mail_message.get('reply'),
578+ 'date': mail_message.get('date'),
579+ 'message_id': mail_message.get('message-id'),
580+ 'subtype': mail_message.get('subtype'),
581+ 'headers': mail_message.get('headers'),
582+ 'state': folder.msg_state,
583+ 'attachment_ids': [(6, 0, attachments)],
584+ },
585+ context))
586+
587+ if folder.delete_matching:
588+ connection.store(msgid, '+FLAGS', '\\DELETED')
589+ return mail_message_ids
590+
591+ def button_confirm_login(self, cr, uid, ids, context=None):
592+ retval = super(fetchmail_server, self).button_confirm_login(cr, uid,
593+ ids,
594+ context)
595+
596+ for this in self.browse(cr, uid, ids, context):
597+ this.write({'state': 'draft'})
598+ connection = this.connect()
599+ connection.select()
600+ for folder in this.folder_ids:
601+ if connection.select(folder.path)[0] != 'OK':
602+ raise except_orm(
603+ _('Error'), _('Mailbox %s not found!') %
604+ folder.path)
605+ folder.get_algorithm().search_matches(
606+ cr, uid, folder, browse_null(), '')
607+ connection.close()
608+ this.write({'state': 'done'})
609+
610+ return retval
611+
612+ def fields_view_get(self, cr, user, view_id=None, view_type='form',
613+ context=None, toolbar=False, submenu=False):
614+ result = super(fetchmail_server, self).fields_view_get(
615+ cr, user, view_id, view_type, context, toolbar, submenu)
616+
617+ if view_type == 'form':
618+ view = etree.fromstring(
619+ result['fields']['folder_ids']['views']['form']['arch'])
620+ modifiers = {}
621+ docstr = ''
622+ for algorithm in self.pool.get('fetchmail.server.folder')\
623+ ._get_match_algorithms().itervalues():
624+ for modifier in ['required', 'readonly']:
625+ for field in getattr(algorithm, modifier + '_fields'):
626+ modifiers.setdefault(field, {})
627+ modifiers[field].setdefault(modifier, [])
628+ if modifiers[field][modifier]:
629+ modifiers[field][modifier].insert(0, '|')
630+ modifiers[field][modifier].append(
631+ ("match_algorithm", "==", algorithm.__name__))
632+ docstr += _(algorithm.name) + '\n' + _(algorithm.__doc__) + \
633+ '\n\n'
634+
635+ for field in view:
636+ if field.tag == 'field' and field.get('name') in modifiers:
637+ field.set('modifiers', simplejson.dumps(
638+ dict(
639+ eval(field.attrib['modifiers'],
640+ UnquoteEvalContext({})),
641+ **modifiers[field.attrib['name']])))
642+ if (field.tag == 'field' and
643+ field.get('name') == 'match_algorithm'):
644+ field.set('help', docstr)
645+ result['fields']['folder_ids']['views']['form']['arch'] = \
646+ etree.tostring(view)
647+
648+ return result
649
650=== added file 'fetchmail_attach_from_folder/model/fetchmail_server_folder.py'
651--- fetchmail_attach_from_folder/model/fetchmail_server_folder.py 1970-01-01 00:00:00 +0000
652+++ fetchmail_attach_from_folder/model/fetchmail_server_folder.py 2013-04-18 14:54:31 +0000
653@@ -0,0 +1,120 @@
654+# -*- encoding: utf-8 -*-
655+##############################################################################
656+#
657+# OpenERP, Open Source Management Solution
658+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
659+# All Rights Reserved
660+#
661+# This program is free software: you can redistribute it and/or modify
662+# it under the terms of the GNU Affero General Public License as
663+# published by the Free Software Foundation, either version 3 of the
664+# License, or (at your option) any later version.
665+#
666+# This program is distributed in the hope that it will be useful,
667+# but WITHOUT ANY WARRANTY; without even the implied warranty of
668+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
669+# GNU Affero General Public License for more details.
670+#
671+# You should have received a copy of the GNU Affero General Public License
672+# along with this program. If not, see <http://www.gnu.org/licenses/>.
673+#
674+########################################################################
675+
676+from openerp.osv import fields
677+from openerp.osv.orm import Model
678+from .. import match_algorithm
679+
680+
681+class fetchmail_server_folder(Model):
682+ _name = 'fetchmail.server.folder'
683+ _rec_name = 'path'
684+
685+ def _get_match_algorithms(self):
686+ def get_all_subclasses(cls):
687+ return cls.__subclasses__() + [subsub
688+ for sub in cls.__subclasses__()
689+ for subsub in get_all_subclasses(sub)]
690+ return dict([(cls.__name__, cls) for cls in get_all_subclasses(
691+ match_algorithm.base.base)])
692+
693+ def _get_match_algorithms_sel(self, cr, uid, context=None):
694+ algorithms = []
695+ for cls in self._get_match_algorithms().itervalues():
696+ algorithms.append((cls.__name__, cls.name))
697+ algorithms.sort()
698+ return algorithms
699+
700+ _columns = {
701+ 'sequence': fields.integer('Sequence'),
702+ 'path': fields.char(
703+ 'Path', size=256, help='The path to your mail '
704+ "folder. Typically would be something like 'INBOX.myfolder'",
705+ required=True),
706+ 'model_id': fields.many2one(
707+ 'ir.model', 'Model', required=True,
708+ help='The model to attach emails to'),
709+ 'model_field': fields.char(
710+ 'Field (model)', size=128,
711+ help='The field in your model that contains the field to match '
712+ 'against.\n'
713+ 'Examples:\n'
714+ "'email' if your model is res.partner, or "
715+ "'partner_id.email' if you're matching sale orders"),
716+ 'model_order': fields.char(
717+ 'Order (model)', size=128,
718+ help='Fields to order by, this mostly useful in conjunction '
719+ "with 'Use 1st match'"),
720+ 'match_algorithm': fields.selection(
721+ _get_match_algorithms_sel,
722+ 'Match algorithm', required=True, translate=True,
723+ help='The algorithm used to determine which object an email '
724+ 'matches.'),
725+ 'mail_field': fields.char(
726+ 'Field (email)', size=128,
727+ help='The field in the email used for matching. Typically '
728+ "this is 'to' or 'from'"),
729+ 'server_id': fields.many2one('fetchmail.server', 'Server'),
730+ 'delete_matching': fields.boolean(
731+ 'Delete matches',
732+ help='Delete matched emails from server'),
733+ 'flag_nonmatching': fields.boolean(
734+ 'Flag nonmatching',
735+ help="Flag emails in the server that don't match any object "
736+ 'in OpenERP'),
737+ 'match_first': fields.boolean(
738+ 'Use 1st match',
739+ help='If there are multiple matches, use the first one. If '
740+ 'not checked, multiple matches count as no match at all'),
741+ 'domain': fields.char(
742+ 'Domain', size=128, help='Fill in a search '
743+ 'filter to narrow down objects to match'),
744+ 'msg_state': fields.selection(
745+ [
746+ ('sent', 'Sent'),
747+ ('received', 'Received'),
748+ ],
749+ 'Message state',
750+ help='The state messages fetched from this folder should be '
751+ 'assigned in OpenERP'),
752+ }
753+
754+ _defaults = {
755+ 'flag_nonmatching': True,
756+ 'msg_state': 'received',
757+ }
758+
759+ def get_algorithm(self, cr, uid, ids, context=None):
760+ for this in self.browse(cr, uid, ids, context):
761+ return self._get_match_algorithms()[this.match_algorithm]()
762+
763+ def button_attach_mail_manually(self, cr, uid, ids, context=None):
764+ for this in self.browse(cr, uid, ids, context):
765+ context.update({'default_folder_id': this.id})
766+ return {
767+ 'type': 'ir.actions.act_window',
768+ 'res_model': 'fetchmail.attach.mail.manually',
769+ 'target': 'new',
770+ 'context': context,
771+ 'view_type': 'form',
772+ 'view_mode': 'form',
773+ }
774
775=== added directory 'fetchmail_attach_from_folder/view'
776=== added file 'fetchmail_attach_from_folder/view/fetchmail_server.xml'
777--- fetchmail_attach_from_folder/view/fetchmail_server.xml 1970-01-01 00:00:00 +0000
778+++ fetchmail_attach_from_folder/view/fetchmail_server.xml 2013-04-18 14:54:31 +0000
779@@ -0,0 +1,58 @@
780+<?xml version="1.0" encoding="utf-8"?>
781+<openerp>
782+ <data>
783+ <record model="ir.ui.view" id="view_email_server_form">
784+ <field name="name">fetchmail.server.form</field>
785+ <field name="model">fetchmail.server</field>
786+ <field name="type">form</field>
787+ <field name="inherit_id" ref="fetchmail.view_email_server_form" />
788+ <field name="arch" type="xml">
789+ <data>
790+ <field name="object_id" position="attributes">
791+ <attribute name="attrs">{'required': [('type', '!=', 'imap')]}</attribute>
792+ </field>
793+ <xpath expr="//page[@string='Server &amp; Login']/group[3]" position="after">
794+ <group col="2" colspan="2" attrs="{'invisible': [('type','!=','imap')]}">
795+ <separator string="Folders to monitor" colspan="2"/>
796+ <field
797+ name="folder_ids"
798+ nolabel="1"
799+ colspan="2"
800+ on_change="onchange_server_type(type, is_ssl, object_id)">
801+ <tree>
802+ <field name="sequence" invisible="1" />
803+ <field name="path" />
804+ <field name="model_id" />
805+ <field name="model_field" />
806+ <field name="match_algorithm" />
807+ <field name="mail_field" />
808+ </tree>
809+ <form col="6">
810+ <field name="path" />
811+ <field name="model_id" />
812+ <field name="model_field" />
813+ <field name="match_algorithm" />
814+ <field name="mail_field" />
815+ <newline />
816+ <field name="delete_matching" />
817+ <field name="flag_nonmatching" />
818+ <field name="match_first" />
819+ <field name="msg_state" />
820+ <field name="model_order" attrs="{'readonly': [('match_first','==',False)], 'required': [('match_first','==',True)]}" />
821+ <field name="domain" />
822+ <newline />
823+ <label />
824+ <label />
825+ <label />
826+ <label />
827+ <label />
828+ <button type="object" name="button_attach_mail_manually" string="Attach mail manually" icon="gtk-redo" />
829+ </form>
830+ </field>
831+ </group>
832+ </xpath>
833+ </data>
834+ </field>
835+ </record>
836+ </data>
837+</openerp>
838
839=== added directory 'fetchmail_attach_from_folder/wizard'
840=== added file 'fetchmail_attach_from_folder/wizard/__init__.py'
841--- fetchmail_attach_from_folder/wizard/__init__.py 1970-01-01 00:00:00 +0000
842+++ fetchmail_attach_from_folder/wizard/__init__.py 2013-04-18 14:54:31 +0000
843@@ -0,0 +1,23 @@
844+# -*- encoding: utf-8 -*-
845+##############################################################################
846+#
847+# OpenERP, Open Source Management Solution
848+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
849+# All Rights Reserved
850+#
851+# This program is free software: you can redistribute it and/or modify
852+# it under the terms of the GNU Affero General Public License as
853+# published by the Free Software Foundation, either version 3 of the
854+# License, or (at your option) any later version.
855+#
856+# This program is distributed in the hope that it will be useful,
857+# but WITHOUT ANY WARRANTY; without even the implied warranty of
858+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
859+# GNU Affero General Public License for more details.
860+#
861+# You should have received a copy of the GNU Affero General Public License
862+# along with this program. If not, see <http://www.gnu.org/licenses/>.
863+#
864+##############################################################################
865+
866+import attach_mail_manually
867
868=== added file 'fetchmail_attach_from_folder/wizard/attach_mail_manually.py'
869--- fetchmail_attach_from_folder/wizard/attach_mail_manually.py 1970-01-01 00:00:00 +0000
870+++ fetchmail_attach_from_folder/wizard/attach_mail_manually.py 2013-04-18 14:54:31 +0000
871@@ -0,0 +1,110 @@
872+# -*- encoding: utf-8 -*-
873+##############################################################################
874+#
875+# OpenERP, Open Source Management Solution
876+# This module copyright (C) 2013 Therp BV (<http://therp.nl>)
877+# All Rights Reserved
878+#
879+# This program is free software: you can redistribute it and/or modify
880+# it under the terms of the GNU Affero General Public License as
881+# published by the Free Software Foundation, either version 3 of the
882+# License, or (at your option) any later version.
883+#
884+# This program is distributed in the hope that it will be useful,
885+# but WITHOUT ANY WARRANTY; without even the implied warranty of
886+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
887+# GNU Affero General Public License for more details.
888+#
889+# You should have received a copy of the GNU Affero General Public License
890+# along with this program. If not, see <http://www.gnu.org/licenses/>.
891+#
892+##############################################################################
893+
894+from openerp.osv import fields
895+from openerp.osv.orm import TransientModel
896+
897+
898+class attach_mail_manually(TransientModel):
899+ _name = 'fetchmail.attach.mail.manually'
900+
901+ _columns = {
902+ 'folder_id': fields.many2one('fetchmail.server.folder', 'Folder',
903+ readonly=True),
904+ 'mail_ids': fields.one2many(
905+ 'fetchmail.attach.mail.manually.mail', 'wizard_id', 'Emails'),
906+ }
907+
908+ def default_get(self, cr, uid, fields_list, context=None):
909+ if context is None:
910+ context = {}
911+
912+ defaults = super(attach_mail_manually, self).default_get(cr, uid,
913+ fields_list, context)
914+
915+ for folder in self.pool.get('fetchmail.server.folder').browse(cr, uid,
916+ [context.get('default_folder_id')], context):
917+ defaults['mail_ids']=[]
918+ connection = folder.server_id.connect()
919+ connection.select(folder.path)
920+ result, msgids = connection.search(None,
921+ 'FLAGGED' if folder.flag_nonmatching else 'UNDELETED')
922+ if result != 'OK':
923+ logger.error('Could not search mailbox %s on %s' % (
924+ folder.path, this.server))
925+ continue
926+ attach_mail_manually_mail._columns['object_id'].selection=[
927+ (folder.model_id.model, folder.model_id.name)]
928+ for msgid in msgids[0].split():
929+ result, msgdata = connection.fetch(msgid, '(RFC822)')
930+ if result != 'OK':
931+ logger.error('Could not fetch %s in %s on %s' % (
932+ msgid, folder.path, this.server))
933+ continue
934+ mail_message = self.pool.get('mail.message').parse_message(
935+ msgdata[0][1])
936+ defaults['mail_ids'].append((0, 0, {
937+ 'msgid': msgid,
938+ 'subject': mail_message.get('subject', ''),
939+ 'date': mail_message.get('date', ''),
940+ 'object_id': folder.model_id.model+',False'
941+ }))
942+ connection.close()
943+
944+ return defaults
945+
946+ def attach_mails(self, cr, uid, ids, context=None):
947+ for this in self.browse(cr, uid, ids, context):
948+ for mail in this.mail_ids:
949+ connection = this.folder_id.server_id.connect()
950+ connection.select(this.folder_id.path)
951+ result, msgdata = connection.fetch(mail.msgid, '(RFC822)')
952+ if result != 'OK':
953+ logger.error('Could not fetch %s in %s on %s' % (
954+ msgid, folder.path, this.server))
955+ continue
956+
957+ mail_message = self.pool.get('mail.message').parse_message(
958+ msgdata[0][1], this.folder_id.server_id.original)
959+
960+ this.folder_id.server_id.attach_mail(connection,
961+ mail.object_id.id, this.folder_id, mail_message,
962+ mail.msgid)
963+ connection.close()
964+ return {'type': 'ir.actions.act_window_close'}
965+
966+class attach_mail_manually_mail(TransientModel):
967+ _name = 'fetchmail.attach.mail.manually.mail'
968+
969+ _columns = {
970+ 'wizard_id': fields.many2one('fetchmail.attach.mail.manually',
971+ readonly=True),
972+ 'msgid': fields.char('Message id', size=16, readonly=True),
973+ 'subject': fields.char('Subject', size=128, readonly=True),
974+ 'date': fields.datetime('Date', readonly=True),
975+ 'object_id': fields.reference('Object',
976+ selection=lambda self, cr, uid, context:
977+ [(m.model, m.name) for m in
978+ self.pool.get('ir.model').browse(cr, uid,
979+ self.pool.get('ir.model').search(cr, uid, []),
980+ context)], size=128),
981+ }
982
983=== added file 'fetchmail_attach_from_folder/wizard/attach_mail_manually.xml'
984--- fetchmail_attach_from_folder/wizard/attach_mail_manually.xml 1970-01-01 00:00:00 +0000
985+++ fetchmail_attach_from_folder/wizard/attach_mail_manually.xml 2013-04-18 14:54:31 +0000
986@@ -0,0 +1,26 @@
987+<?xml version="1.0" encoding="utf-8"?>
988+<openerp>
989+ <data>
990+ <record model="ir.ui.view" id="view_attach_mail_manually">
991+ <field name="name">fetchmail.attach.mail.manually</field>
992+ <field name="model">fetchmail.attach.mail.manually</field>
993+ <field name="type">form</field>
994+ <field name="arch" type="xml">
995+ <form col="4">
996+ <field name="folder_id" colspan="4" />
997+ <field name="mail_ids" nolabel="1" colspan="4">
998+ <tree editable="top">
999+ <field name="subject" />
1000+ <field name="date" />
1001+ <field name="object_id" />
1002+ </tree>
1003+ </field>
1004+ <label string="" />
1005+ <label string="" />
1006+ <button special="cancel" string="Cancel" icon="gtk-cancel" />
1007+ <button string="Save" type="object" name="attach_mails" icon="gtk-ok" />
1008+ </form>
1009+ </field>
1010+ </record>
1011+ </data>
1012+</openerp>

Subscribers

People subscribed via source and target branches