We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Emails sent by the `mail` callback pluging are missing the `Date` header.
### Summary

When the `mail` callback plugin successfully sends an email, the receiver cannot find it in his mailbox.
This is because mailboxes are usually sorted by origination date and this field is missing in the emails sent by this plugin.

According to [RFC 5322, the `Date` header is mandatory](https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.1) but the `mail` callback plugin sends emails without it.

>  The only required header fields are the origination date field and
   the originator address field(s).

### Issue Type

Bug Report

### Component Name

`community.general.mail` callback plugin

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.1]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/lenaic/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.10/site-packages/ansible
  ansible collection location = /home/lenaic/.ansible/collections:/usr/share/ansible/collections
  executable location = /bin/ansible
  python version = 3.10.1 (main, Dec 18 2021, 23:53:45) [GCC 11.1.0]
  jinja version = 3.0.3
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general

# /usr/lib/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.3.0  
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
CACHE_PLUGIN(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ~/.ansible/tmp/facts_cache
CACHE_PLUGIN_TIMEOUT(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = 86400
CALLBACKS_ENABLED(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ['community.general.mail']
DEFAULT_FORCE_HANDLERS(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = True
DEFAULT_GATHERING(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = smart
DEFAULT_HOST_LIST(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ['/home/lenaic/doc/devel/perso/lenaic_config/inventory-libvirt.yaml']
DEFAULT_VAULT_PASSWORD_FILE(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = /home/lenaic/doc/devel/perso/lenaic_config/.vault_password
INVENTORY_ENABLED(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ['community.libvirt.libvirt']
RETRY_FILES_ENABLED(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = False
```


### OS / Environment

```console
$ cat /etc/os-release 
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://bugs.archlinux.org/"
LOGO=archlinux-logo
```

### Steps to Reproduce

Enable the `community.general.mail` callback plugin thanks to the following configuration options

```ini
[defaults]
callbacks_enabled = community.general.mail

[callback_mail]
to = me <me@my.mail.provider>
sender = Ansible <noreply@my.mail.provider>
```

And try to launch a playbook some tasks of which are failing.

### Expected Results

The email containing the Ansible failures appears in the receiver mailbox at the top with the date at which the playbook execution finished.

### Actual Results

The received email is invisible because it is sorted as if it had been received the 1st January 1970 00:00:00.

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `plugins/callback/mail.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 # Copyright: (c) 2012, Dag Wieers <dag@wieers.com>
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 from __future__ import (absolute_import, division, print_function)
7 __metaclass__ = type
8 
9 DOCUMENTATION = '''
10 name: mail
11 type: notification
12 short_description: Sends failure events via email
13 description:
14 - This callback will report failures via email
15 author:
16 - Dag Wieers (@dagwieers)
17 requirements:
18 - whitelisting in configuration
19 options:
20   mta:
21     description: Mail Transfer Agent, server that accepts SMTP
22     env:
23         - name: SMTPHOST
24     ini:
25         - section: callback_mail
26           key: smtphost
27     default: localhost
28   mtaport:
29     description: Mail Transfer Agent Port, port at which server SMTP
30     ini:
31         - section: callback_mail
32           key: smtpport
33     default: 25
34   to:
35     description: Mail recipient
36     ini:
37         - section: callback_mail
38           key: to
39     default: root
40   sender:
41     description: Mail sender
42     ini:
43         - section: callback_mail
44           key: sender
45   cc:
46     description: CC'd recipient
47     ini:
48         - section: callback_mail
49           key: cc
50   bcc:
51     description: BCC'd recipient
52     ini:
53         - section: callback_mail
54           key: bcc
55 notes:
56 - "TODO: expand configuration options now that plugins can leverage Ansible's configuration"
57 '''
58 
59 import json
60 import os
61 import re
62 import smtplib
63 
64 from ansible.module_utils.six import string_types
65 from ansible.module_utils.common.text.converters import to_bytes
66 from ansible.parsing.ajson import AnsibleJSONEncoder
67 from ansible.plugins.callback import CallbackBase
68 
69 
70 class CallbackModule(CallbackBase):
71     ''' This Ansible callback plugin mails errors to interested parties. '''
72     CALLBACK_VERSION = 2.0
73     CALLBACK_TYPE = 'notification'
74     CALLBACK_NAME = 'community.general.mail'
75     CALLBACK_NEEDS_WHITELIST = True
76 
77     def __init__(self, display=None):
78         super(CallbackModule, self).__init__(display=display)
79         self.sender = None
80         self.to = 'root'
81         self.smtphost = os.getenv('SMTPHOST', 'localhost')
82         self.smtpport = 25
83         self.cc = None
84         self.bcc = None
85 
86     def set_options(self, task_keys=None, var_options=None, direct=None):
87 
88         super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
89 
90         self.sender = self.get_option('sender')
91         self.to = self.get_option('to')
92         self.smtphost = self.get_option('mta')
93         self.smtpport = int(self.get_option('mtaport'))
94         self.cc = self.get_option('cc')
95         self.bcc = self.get_option('bcc')
96 
97     def mail(self, subject='Ansible error mail', body=None):
98         if body is None:
99             body = subject
100 
101         smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)
102 
103         content = 'From: %s\n' % self.sender
104         content += 'To: %s\n' % self.to
105         if self.cc:
106             content += 'Cc: %s\n' % self.cc
107         content += 'Subject: %s\n\n' % subject.strip()
108         content += body
109 
110         addresses = self.to.split(',')
111         if self.cc:
112             addresses += self.cc.split(',')
113         if self.bcc:
114             addresses += self.bcc.split(',')
115 
116         for address in addresses:
117             smtp.sendmail(self.sender, address, to_bytes(content))
118 
119         smtp.quit()
120 
121     def subject_msg(self, multiline, failtype, linenr):
122         return '%s: %s' % (failtype, multiline.strip('\r\n').splitlines()[linenr])
123 
124     def indent(self, multiline, indent=8):
125         return re.sub('^', ' ' * indent, multiline, flags=re.MULTILINE)
126 
127     def body_blob(self, multiline, texttype):
128         ''' Turn some text output in a well-indented block for sending in a mail body '''
129         intro = 'with the following %s:\n\n' % texttype
130         blob = ''
131         for line in multiline.strip('\r\n').splitlines():
132             blob += '%s\n' % line
133         return intro + self.indent(blob) + '\n'
134 
135     def mail_result(self, result, failtype):
136         host = result._host.get_name()
137         if not self.sender:
138             self.sender = '"Ansible: %s" <root>' % host
139 
140         # Add subject
141         if self.itembody:
142             subject = self.itemsubject
143         elif result._result.get('failed_when_result') is True:
144             subject = "Failed due to 'failed_when' condition"
145         elif result._result.get('msg'):
146             subject = self.subject_msg(result._result['msg'], failtype, 0)
147         elif result._result.get('stderr'):
148             subject = self.subject_msg(result._result['stderr'], failtype, -1)
149         elif result._result.get('stdout'):
150             subject = self.subject_msg(result._result['stdout'], failtype, -1)
151         elif result._result.get('exception'):  # Unrelated exceptions are added to output :-/
152             subject = self.subject_msg(result._result['exception'], failtype, -1)
153         else:
154             subject = '%s: %s' % (failtype, result._task.name or result._task.action)
155 
156         # Make playbook name visible (e.g. in Outlook/Gmail condensed view)
157         body = 'Playbook: %s\n' % os.path.basename(self.playbook._file_name)
158         if result._task.name:
159             body += 'Task: %s\n' % result._task.name
160         body += 'Module: %s\n' % result._task.action
161         body += 'Host: %s\n' % host
162         body += '\n'
163 
164         # Add task information (as much as possible)
165         body += 'The following task failed:\n\n'
166         if 'invocation' in result._result:
167             body += self.indent('%s: %s\n' % (result._task.action, json.dumps(result._result['invocation']['module_args'], indent=4)))
168         elif result._task.name:
169             body += self.indent('%s (%s)\n' % (result._task.name, result._task.action))
170         else:
171             body += self.indent('%s\n' % result._task.action)
172         body += '\n'
173 
174         # Add item / message
175         if self.itembody:
176             body += self.itembody
177         elif result._result.get('failed_when_result') is True:
178             body += "due to the following condition:\n\n" + self.indent('failed_when:\n- ' + '\n- '.join(result._task.failed_when)) + '\n\n'
179         elif result._result.get('msg'):
180             body += self.body_blob(result._result['msg'], 'message')
181 
182         # Add stdout / stderr / exception / warnings / deprecations
183         if result._result.get('stdout'):
184             body += self.body_blob(result._result['stdout'], 'standard output')
185         if result._result.get('stderr'):
186             body += self.body_blob(result._result['stderr'], 'error output')
187         if result._result.get('exception'):  # Unrelated exceptions are added to output :-/
188             body += self.body_blob(result._result['exception'], 'exception')
189         if result._result.get('warnings'):
190             for i in range(len(result._result.get('warnings'))):
191                 body += self.body_blob(result._result['warnings'][i], 'exception %d' % (i + 1))
192         if result._result.get('deprecations'):
193             for i in range(len(result._result.get('deprecations'))):
194                 body += self.body_blob(result._result['deprecations'][i], 'exception %d' % (i + 1))
195 
196         body += 'and a complete dump of the error:\n\n'
197         body += self.indent('%s: %s' % (failtype, json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)))
198 
199         self.mail(subject=subject, body=body)
200 
201     def v2_playbook_on_start(self, playbook):
202         self.playbook = playbook
203         self.itembody = ''
204 
205     def v2_runner_on_failed(self, result, ignore_errors=False):
206         if ignore_errors:
207             return
208 
209         self.mail_result(result, 'Failed')
210 
211     def v2_runner_on_unreachable(self, result):
212         self.mail_result(result, 'Unreachable')
213 
214     def v2_runner_on_async_failed(self, result):
215         self.mail_result(result, 'Async failure')
216 
217     def v2_runner_item_on_failed(self, result):
218         # Pass item information to task failure
219         self.itemsubject = result._result['msg']
220         self.itembody += self.body_blob(json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), "failed item dump '%(item)s'" % result._result)
221
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/plugins/callback/mail.py b/plugins/callback/mail.py
--- a/plugins/callback/mail.py
+++ b/plugins/callback/mail.py
@@ -59,6 +59,7 @@
 import json
 import os
 import re
+import email.utils
 import smtplib
 
 from ansible.module_utils.six import string_types
@@ -100,10 +101,12 @@
 
         smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)
 
-        content = 'From: %s\n' % self.sender
+        content = 'Date: %s\n' % email.utils.formatdate()
+        content += 'From: %s\n' % self.sender
         content += 'To: %s\n' % self.to
         if self.cc:
             content += 'Cc: %s\n' % self.cc
+        content += 'Message-ID: %s\n' % email.utils.make_msgid()
         content += 'Subject: %s\n\n' % subject.strip()
         content += body

{"golden_diff": "diff --git a/plugins/callback/mail.py b/plugins/callback/mail.py\n--- a/plugins/callback/mail.py\n+++ b/plugins/callback/mail.py\n@@ -59,6 +59,7 @@\n import json\n import os\n import re\n+import email.utils\n import smtplib\n \n from ansible.module_utils.six import string_types\n@@ -100,10 +101,12 @@\n \n         smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)\n \n-        content = 'From: %s\\n' % self.sender\n+        content = 'Date: %s\\n' % email.utils.formatdate()\n+        content += 'From: %s\\n' % self.sender\n         content += 'To: %s\\n' % self.to\n         if self.cc:\n             content += 'Cc: %s\\n' % self.cc\n+        content += 'Message-ID: %s\\n' % email.utils.make_msgid()\n         content += 'Subject: %s\\n\\n' % subject.strip()\n         content += body\n", "issue": "Emails sent by the `mail` callback pluging are missing the `Date` header.\n### Summary\n\nWhen the `mail` callback plugin successfully sends an email, the receiver cannot find it in his mailbox.\r\nThis is because mailboxes are usually sorted by origination date and this field is missing in the emails sent by this plugin.\r\n\r\nAccording to [RFC 5322, the `Date` header is mandatory](https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.1) but the `mail` callback plugin sends emails without it.\r\n\r\n>  The only required header fields are the origination date field and\r\n   the originator address field(s).\n\n### Issue Type\n\nBug Report\n\n### Component Name\n\n`community.general.mail` callback plugin\n\n### Ansible Version\n\n```console (paste below)\r\n$ ansible --version\r\nansible [core 2.12.1]\r\n  config file = /etc/ansible/ansible.cfg\r\n  configured module search path = ['/home/lenaic/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']\r\n  ansible python module location = /usr/lib/python3.10/site-packages/ansible\r\n  ansible collection location = /home/lenaic/.ansible/collections:/usr/share/ansible/collections\r\n  executable location = /bin/ansible\r\n  python version = 3.10.1 (main, Dec 18 2021, 23:53:45) [GCC 11.1.0]\r\n  jinja version = 3.0.3\r\n  libyaml = True\r\n```\r\n\n\n### Community.general Version\n\n```console (paste below)\r\n$ ansible-galaxy collection list community.general\r\n\r\n# /usr/lib/python3.10/site-packages/ansible_collections\r\nCollection        Version\r\n----------------- -------\r\ncommunity.general 4.3.0  \r\n```\r\n\n\n### Configuration\n\n```console (paste below)\r\n$ ansible-config dump --only-changed\r\nCACHE_PLUGIN(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = jsonfile\r\nCACHE_PLUGIN_CONNECTION(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ~/.ansible/tmp/facts_cache\r\nCACHE_PLUGIN_TIMEOUT(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = 86400\r\nCALLBACKS_ENABLED(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ['community.general.mail']\r\nDEFAULT_FORCE_HANDLERS(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = True\r\nDEFAULT_GATHERING(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = smart\r\nDEFAULT_HOST_LIST(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ['/home/lenaic/doc/devel/perso/lenaic_config/inventory-libvirt.yaml']\r\nDEFAULT_VAULT_PASSWORD_FILE(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = /home/lenaic/doc/devel/perso/lenaic_config/.vault_password\r\nINVENTORY_ENABLED(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = ['community.libvirt.libvirt']\r\nRETRY_FILES_ENABLED(/home/lenaic/doc/devel/perso/lenaic_config/ansible-libvirt.cfg) = False\r\n```\r\n\n\n### OS / Environment\n\n```console\r\n$ cat /etc/os-release \r\nNAME=\"Arch Linux\"\r\nPRETTY_NAME=\"Arch Linux\"\r\nID=arch\r\nBUILD_ID=rolling\r\nANSI_COLOR=\"38;2;23;147;209\"\r\nHOME_URL=\"https://archlinux.org/\"\r\nDOCUMENTATION_URL=\"https://wiki.archlinux.org/\"\r\nSUPPORT_URL=\"https://bbs.archlinux.org/\"\r\nBUG_REPORT_URL=\"https://bugs.archlinux.org/\"\r\nLOGO=archlinux-logo\r\n```\n\n### Steps to Reproduce\n\nEnable the `community.general.mail` callback plugin thanks to the following configuration options\r\n\r\n```ini\r\n[defaults]\r\ncallbacks_enabled = community.general.mail\r\n\r\n[callback_mail]\r\nto = me <me@my.mail.provider>\r\nsender = Ansible <noreply@my.mail.provider>\r\n```\r\n\r\nAnd try to launch a playbook some tasks of which are failing.\n\n### Expected Results\n\nThe email containing the Ansible failures appears in the receiver mailbox at the top with the date at which the playbook execution finished.\n\n### Actual Results\n\nThe received email is invisible because it is sorted as if it had been received the 1st January 1970 00:00:00.\n\n### Code of Conduct\n\n- [X] I agree to follow the Ansible Code of Conduct\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Copyright: (c) 2012, Dag Wieers <dag@wieers.com>\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\nDOCUMENTATION = '''\nname: mail\ntype: notification\nshort_description: Sends failure events via email\ndescription:\n- This callback will report failures via email\nauthor:\n- Dag Wieers (@dagwieers)\nrequirements:\n- whitelisting in configuration\noptions:\n  mta:\n    description: Mail Transfer Agent, server that accepts SMTP\n    env:\n        - name: SMTPHOST\n    ini:\n        - section: callback_mail\n          key: smtphost\n    default: localhost\n  mtaport:\n    description: Mail Transfer Agent Port, port at which server SMTP\n    ini:\n        - section: callback_mail\n          key: smtpport\n    default: 25\n  to:\n    description: Mail recipient\n    ini:\n        - section: callback_mail\n          key: to\n    default: root\n  sender:\n    description: Mail sender\n    ini:\n        - section: callback_mail\n          key: sender\n  cc:\n    description: CC'd recipient\n    ini:\n        - section: callback_mail\n          key: cc\n  bcc:\n    description: BCC'd recipient\n    ini:\n        - section: callback_mail\n          key: bcc\nnotes:\n- \"TODO: expand configuration options now that plugins can leverage Ansible's configuration\"\n'''\n\nimport json\nimport os\nimport re\nimport smtplib\n\nfrom ansible.module_utils.six import string_types\nfrom ansible.module_utils.common.text.converters import to_bytes\nfrom ansible.parsing.ajson import AnsibleJSONEncoder\nfrom ansible.plugins.callback import CallbackBase\n\n\nclass CallbackModule(CallbackBase):\n    ''' This Ansible callback plugin mails errors to interested parties. '''\n    CALLBACK_VERSION = 2.0\n    CALLBACK_TYPE = 'notification'\n    CALLBACK_NAME = 'community.general.mail'\n    CALLBACK_NEEDS_WHITELIST = True\n\n    def __init__(self, display=None):\n        super(CallbackModule, self).__init__(display=display)\n        self.sender = None\n        self.to = 'root'\n        self.smtphost = os.getenv('SMTPHOST', 'localhost')\n        self.smtpport = 25\n        self.cc = None\n        self.bcc = None\n\n    def set_options(self, task_keys=None, var_options=None, direct=None):\n\n        super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)\n\n        self.sender = self.get_option('sender')\n        self.to = self.get_option('to')\n        self.smtphost = self.get_option('mta')\n        self.smtpport = int(self.get_option('mtaport'))\n        self.cc = self.get_option('cc')\n        self.bcc = self.get_option('bcc')\n\n    def mail(self, subject='Ansible error mail', body=None):\n        if body is None:\n            body = subject\n\n        smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)\n\n        content = 'From: %s\\n' % self.sender\n        content += 'To: %s\\n' % self.to\n        if self.cc:\n            content += 'Cc: %s\\n' % self.cc\n        content += 'Subject: %s\\n\\n' % subject.strip()\n        content += body\n\n        addresses = self.to.split(',')\n        if self.cc:\n            addresses += self.cc.split(',')\n        if self.bcc:\n            addresses += self.bcc.split(',')\n\n        for address in addresses:\n            smtp.sendmail(self.sender, address, to_bytes(content))\n\n        smtp.quit()\n\n    def subject_msg(self, multiline, failtype, linenr):\n        return '%s: %s' % (failtype, multiline.strip('\\r\\n').splitlines()[linenr])\n\n    def indent(self, multiline, indent=8):\n        return re.sub('^', ' ' * indent, multiline, flags=re.MULTILINE)\n\n    def body_blob(self, multiline, texttype):\n        ''' Turn some text output in a well-indented block for sending in a mail body '''\n        intro = 'with the following %s:\\n\\n' % texttype\n        blob = ''\n        for line in multiline.strip('\\r\\n').splitlines():\n            blob += '%s\\n' % line\n        return intro + self.indent(blob) + '\\n'\n\n    def mail_result(self, result, failtype):\n        host = result._host.get_name()\n        if not self.sender:\n            self.sender = '\"Ansible: %s\" <root>' % host\n\n        # Add subject\n        if self.itembody:\n            subject = self.itemsubject\n        elif result._result.get('failed_when_result') is True:\n            subject = \"Failed due to 'failed_when' condition\"\n        elif result._result.get('msg'):\n            subject = self.subject_msg(result._result['msg'], failtype, 0)\n        elif result._result.get('stderr'):\n            subject = self.subject_msg(result._result['stderr'], failtype, -1)\n        elif result._result.get('stdout'):\n            subject = self.subject_msg(result._result['stdout'], failtype, -1)\n        elif result._result.get('exception'):  # Unrelated exceptions are added to output :-/\n            subject = self.subject_msg(result._result['exception'], failtype, -1)\n        else:\n            subject = '%s: %s' % (failtype, result._task.name or result._task.action)\n\n        # Make playbook name visible (e.g. in Outlook/Gmail condensed view)\n        body = 'Playbook: %s\\n' % os.path.basename(self.playbook._file_name)\n        if result._task.name:\n            body += 'Task: %s\\n' % result._task.name\n        body += 'Module: %s\\n' % result._task.action\n        body += 'Host: %s\\n' % host\n        body += '\\n'\n\n        # Add task information (as much as possible)\n        body += 'The following task failed:\\n\\n'\n        if 'invocation' in result._result:\n            body += self.indent('%s: %s\\n' % (result._task.action, json.dumps(result._result['invocation']['module_args'], indent=4)))\n        elif result._task.name:\n            body += self.indent('%s (%s)\\n' % (result._task.name, result._task.action))\n        else:\n            body += self.indent('%s\\n' % result._task.action)\n        body += '\\n'\n\n        # Add item / message\n        if self.itembody:\n            body += self.itembody\n        elif result._result.get('failed_when_result') is True:\n            body += \"due to the following condition:\\n\\n\" + self.indent('failed_when:\\n- ' + '\\n- '.join(result._task.failed_when)) + '\\n\\n'\n        elif result._result.get('msg'):\n            body += self.body_blob(result._result['msg'], 'message')\n\n        # Add stdout / stderr / exception / warnings / deprecations\n        if result._result.get('stdout'):\n            body += self.body_blob(result._result['stdout'], 'standard output')\n        if result._result.get('stderr'):\n            body += self.body_blob(result._result['stderr'], 'error output')\n        if result._result.get('exception'):  # Unrelated exceptions are added to output :-/\n            body += self.body_blob(result._result['exception'], 'exception')\n        if result._result.get('warnings'):\n            for i in range(len(result._result.get('warnings'))):\n                body += self.body_blob(result._result['warnings'][i], 'exception %d' % (i + 1))\n        if result._result.get('deprecations'):\n            for i in range(len(result._result.get('deprecations'))):\n                body += self.body_blob(result._result['deprecations'][i], 'exception %d' % (i + 1))\n\n        body += 'and a complete dump of the error:\\n\\n'\n        body += self.indent('%s: %s' % (failtype, json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)))\n\n        self.mail(subject=subject, body=body)\n\n    def v2_playbook_on_start(self, playbook):\n        self.playbook = playbook\n        self.itembody = ''\n\n    def v2_runner_on_failed(self, result, ignore_errors=False):\n        if ignore_errors:\n            return\n\n        self.mail_result(result, 'Failed')\n\n    def v2_runner_on_unreachable(self, result):\n        self.mail_result(result, 'Unreachable')\n\n    def v2_runner_on_async_failed(self, result):\n        self.mail_result(result, 'Async failure')\n\n    def v2_runner_item_on_failed(self, result):\n        # Pass item information to task failure\n        self.itemsubject = result._result['msg']\n        self.itembody += self.body_blob(json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), \"failed item dump '%(item)s'\" % result._result)\n", "path": "plugins/callback/mail.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Copyright: (c) 2012, Dag Wieers <dag@wieers.com>\n# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)\n\nfrom __future__ import (absolute_import, division, print_function)\n__metaclass__ = type\n\nDOCUMENTATION = '''\nname: mail\ntype: notification\nshort_description: Sends failure events via email\ndescription:\n- This callback will report failures via email\nauthor:\n- Dag Wieers (@dagwieers)\nrequirements:\n- whitelisting in configuration\noptions:\n  mta:\n    description: Mail Transfer Agent, server that accepts SMTP\n    env:\n        - name: SMTPHOST\n    ini:\n        - section: callback_mail\n          key: smtphost\n    default: localhost\n  mtaport:\n    description: Mail Transfer Agent Port, port at which server SMTP\n    ini:\n        - section: callback_mail\n          key: smtpport\n    default: 25\n  to:\n    description: Mail recipient\n    ini:\n        - section: callback_mail\n          key: to\n    default: root\n  sender:\n    description: Mail sender\n    ini:\n        - section: callback_mail\n          key: sender\n  cc:\n    description: CC'd recipient\n    ini:\n        - section: callback_mail\n          key: cc\n  bcc:\n    description: BCC'd recipient\n    ini:\n        - section: callback_mail\n          key: bcc\nnotes:\n- \"TODO: expand configuration options now that plugins can leverage Ansible's configuration\"\n'''\n\nimport json\nimport os\nimport re\nimport email.utils\nimport smtplib\n\nfrom ansible.module_utils.six import string_types\nfrom ansible.module_utils.common.text.converters import to_bytes\nfrom ansible.parsing.ajson import AnsibleJSONEncoder\nfrom ansible.plugins.callback import CallbackBase\n\n\nclass CallbackModule(CallbackBase):\n    ''' This Ansible callback plugin mails errors to interested parties. '''\n    CALLBACK_VERSION = 2.0\n    CALLBACK_TYPE = 'notification'\n    CALLBACK_NAME = 'community.general.mail'\n    CALLBACK_NEEDS_WHITELIST = True\n\n    def __init__(self, display=None):\n        super(CallbackModule, self).__init__(display=display)\n        self.sender = None\n        self.to = 'root'\n        self.smtphost = os.getenv('SMTPHOST', 'localhost')\n        self.smtpport = 25\n        self.cc = None\n        self.bcc = None\n\n    def set_options(self, task_keys=None, var_options=None, direct=None):\n\n        super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)\n\n        self.sender = self.get_option('sender')\n        self.to = self.get_option('to')\n        self.smtphost = self.get_option('mta')\n        self.smtpport = int(self.get_option('mtaport'))\n        self.cc = self.get_option('cc')\n        self.bcc = self.get_option('bcc')\n\n    def mail(self, subject='Ansible error mail', body=None):\n        if body is None:\n            body = subject\n\n        smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)\n\n        content = 'Date: %s\\n' % email.utils.formatdate()\n        content += 'From: %s\\n' % self.sender\n        content += 'To: %s\\n' % self.to\n        if self.cc:\n            content += 'Cc: %s\\n' % self.cc\n        content += 'Message-ID: %s\\n' % email.utils.make_msgid()\n        content += 'Subject: %s\\n\\n' % subject.strip()\n        content += body\n\n        addresses = self.to.split(',')\n        if self.cc:\n            addresses += self.cc.split(',')\n        if self.bcc:\n            addresses += self.bcc.split(',')\n\n        for address in addresses:\n            smtp.sendmail(self.sender, address, to_bytes(content))\n\n        smtp.quit()\n\n    def subject_msg(self, multiline, failtype, linenr):\n        return '%s: %s' % (failtype, multiline.strip('\\r\\n').splitlines()[linenr])\n\n    def indent(self, multiline, indent=8):\n        return re.sub('^', ' ' * indent, multiline, flags=re.MULTILINE)\n\n    def body_blob(self, multiline, texttype):\n        ''' Turn some text output in a well-indented block for sending in a mail body '''\n        intro = 'with the following %s:\\n\\n' % texttype\n        blob = ''\n        for line in multiline.strip('\\r\\n').splitlines():\n            blob += '%s\\n' % line\n        return intro + self.indent(blob) + '\\n'\n\n    def mail_result(self, result, failtype):\n        host = result._host.get_name()\n        if not self.sender:\n            self.sender = '\"Ansible: %s\" <root>' % host\n\n        # Add subject\n        if self.itembody:\n            subject = self.itemsubject\n        elif result._result.get('failed_when_result') is True:\n            subject = \"Failed due to 'failed_when' condition\"\n        elif result._result.get('msg'):\n            subject = self.subject_msg(result._result['msg'], failtype, 0)\n        elif result._result.get('stderr'):\n            subject = self.subject_msg(result._result['stderr'], failtype, -1)\n        elif result._result.get('stdout'):\n            subject = self.subject_msg(result._result['stdout'], failtype, -1)\n        elif result._result.get('exception'):  # Unrelated exceptions are added to output :-/\n            subject = self.subject_msg(result._result['exception'], failtype, -1)\n        else:\n            subject = '%s: %s' % (failtype, result._task.name or result._task.action)\n\n        # Make playbook name visible (e.g. in Outlook/Gmail condensed view)\n        body = 'Playbook: %s\\n' % os.path.basename(self.playbook._file_name)\n        if result._task.name:\n            body += 'Task: %s\\n' % result._task.name\n        body += 'Module: %s\\n' % result._task.action\n        body += 'Host: %s\\n' % host\n        body += '\\n'\n\n        # Add task information (as much as possible)\n        body += 'The following task failed:\\n\\n'\n        if 'invocation' in result._result:\n            body += self.indent('%s: %s\\n' % (result._task.action, json.dumps(result._result['invocation']['module_args'], indent=4)))\n        elif result._task.name:\n            body += self.indent('%s (%s)\\n' % (result._task.name, result._task.action))\n        else:\n            body += self.indent('%s\\n' % result._task.action)\n        body += '\\n'\n\n        # Add item / message\n        if self.itembody:\n            body += self.itembody\n        elif result._result.get('failed_when_result') is True:\n            body += \"due to the following condition:\\n\\n\" + self.indent('failed_when:\\n- ' + '\\n- '.join(result._task.failed_when)) + '\\n\\n'\n        elif result._result.get('msg'):\n            body += self.body_blob(result._result['msg'], 'message')\n\n        # Add stdout / stderr / exception / warnings / deprecations\n        if result._result.get('stdout'):\n            body += self.body_blob(result._result['stdout'], 'standard output')\n        if result._result.get('stderr'):\n            body += self.body_blob(result._result['stderr'], 'error output')\n        if result._result.get('exception'):  # Unrelated exceptions are added to output :-/\n            body += self.body_blob(result._result['exception'], 'exception')\n        if result._result.get('warnings'):\n            for i in range(len(result._result.get('warnings'))):\n                body += self.body_blob(result._result['warnings'][i], 'exception %d' % (i + 1))\n        if result._result.get('deprecations'):\n            for i in range(len(result._result.get('deprecations'))):\n                body += self.body_blob(result._result['deprecations'][i], 'exception %d' % (i + 1))\n\n        body += 'and a complete dump of the error:\\n\\n'\n        body += self.indent('%s: %s' % (failtype, json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)))\n\n        self.mail(subject=subject, body=body)\n\n    def v2_playbook_on_start(self, playbook):\n        self.playbook = playbook\n        self.itembody = ''\n\n    def v2_runner_on_failed(self, result, ignore_errors=False):\n        if ignore_errors:\n            return\n\n        self.mail_result(result, 'Failed')\n\n    def v2_runner_on_unreachable(self, result):\n        self.mail_result(result, 'Unreachable')\n\n    def v2_runner_on_async_failed(self, result):\n        self.mail_result(result, 'Async failure')\n\n    def v2_runner_item_on_failed(self, result):\n        # Pass item information to task failure\n        self.itemsubject = result._result['msg']\n        self.itembody += self.body_blob(json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), \"failed item dump '%(item)s'\" % result._result)\n", "path": "plugins/callback/mail.py"}]}