myl 0.8.7__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,6 +40,6 @@ jobs:
40
40
  echo "$EOF" >> $GITHUB_OUTPUT
41
41
 
42
42
  - name: Create Release
43
- uses: softprops/action-gh-release@v1
43
+ uses: softprops/action-gh-release@v2
44
44
  with:
45
45
  body: ${{ steps.changelog.outputs.changelog }}
@@ -3,3 +3,5 @@
3
3
  *.egg-info
4
4
  build/
5
5
  dist/
6
+ result
7
+ _version.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: myl
3
- Version: 0.8.7
3
+ Version: 0.9.2
4
4
  Summary: Dead simple IMAP CLI client
5
5
  Author-email: Philipp Schmitt <philipp@schmitt.co>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -678,14 +678,19 @@ License: GNU GENERAL PUBLIC LICENSE
678
678
  Public License instead of this License. But first, please read
679
679
  <https://www.gnu.org/licenses/why-not-lgpl.html>.
680
680
 
681
+ Project-URL: homepage, https://github.com/pschmitt/myl
682
+ Project-URL: documentation, https://github.com/pschmitt/myl/blob/head/readme.md
683
+ Project-URL: repository, https://github.com/pschmitt/myl
684
+ Project-URL: issues, https://github.com/pschmitt/myl/issues
681
685
  Keywords: imap,email
682
686
  Classifier: Programming Language :: Python :: 3
683
687
  Requires-Python: >=3.8
684
688
  Description-Content-Type: text/markdown
685
689
  License-File: LICENSE
686
- Requires-Dist: imap-tools==1.5.0
687
- Requires-Dist: myl-discovery==0.5.7
688
- Requires-Dist: rich>=13.7.0
690
+ Requires-Dist: imap-tools<2.0.0,>=1.5.0
691
+ Requires-Dist: myl-discovery>=0.6.1.dev0
692
+ Requires-Dist: rich<14.0.0,>=13.0.0
693
+ Requires-Dist: html2text>=2024.2.26
689
694
 
690
695
  # 📧 myl
691
696
 
@@ -716,15 +721,23 @@ straightforward way to interact with IMAP servers.
716
721
 
717
722
  To install myl, follow these steps:
718
723
 
719
- ```bash
724
+ ```shell
720
725
  pipx install myl
726
+ # or:
727
+ pip install --user myl
728
+ ```
729
+
730
+ on nix you can do this:
731
+
732
+ ```shell
733
+ nix run github:pschmitt/myl -- --help
721
734
  ```
722
735
 
723
736
  ## 🛠️ Usage
724
737
 
725
738
  Here's how you can use myl:
726
739
 
727
- ```bash
740
+ ```shell
728
741
  myl --help
729
742
  ```
730
743
 
@@ -732,7 +745,7 @@ This command will display the help information for the `myl` command.
732
745
 
733
746
  Here are some examples of using flags with the `myl` command:
734
747
 
735
- ```bash
748
+ ```shell
736
749
  # Connect to an IMAP server
737
750
  myl --server imap.example.com --port 143 --starttls --username "$username" --password "$password"
738
751
 
@@ -27,15 +27,23 @@ straightforward way to interact with IMAP servers.
27
27
 
28
28
  To install myl, follow these steps:
29
29
 
30
- ```bash
30
+ ```shell
31
31
  pipx install myl
32
+ # or:
33
+ pip install --user myl
34
+ ```
35
+
36
+ on nix you can do this:
37
+
38
+ ```shell
39
+ nix run github:pschmitt/myl -- --help
32
40
  ```
33
41
 
34
42
  ## 🛠️ Usage
35
43
 
36
44
  Here's how you can use myl:
37
45
 
38
- ```bash
46
+ ```shell
39
47
  myl --help
40
48
  ```
41
49
 
@@ -43,7 +51,7 @@ This command will display the help information for the `myl` command.
43
51
 
44
52
  Here are some examples of using flags with the `myl` command:
45
53
 
46
- ```bash
54
+ ```shell
47
55
  # Connect to an IMAP server
48
56
  myl --server imap.example.com --port 143 --starttls --username "$username" --password "$password"
49
57
 
myl-0.9.2/flake.lock ADDED
@@ -0,0 +1,116 @@
1
+ {
2
+ "nodes": {
3
+ "flake-utils": {
4
+ "inputs": {
5
+ "systems": "systems"
6
+ },
7
+ "locked": {
8
+ "lastModified": 1726560853,
9
+ "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
10
+ "owner": "numtide",
11
+ "repo": "flake-utils",
12
+ "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
13
+ "type": "github"
14
+ },
15
+ "original": {
16
+ "owner": "numtide",
17
+ "repo": "flake-utils",
18
+ "type": "github"
19
+ }
20
+ },
21
+ "flake-utils_2": {
22
+ "inputs": {
23
+ "systems": "systems_2"
24
+ },
25
+ "locked": {
26
+ "lastModified": 1726560853,
27
+ "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
28
+ "owner": "numtide",
29
+ "repo": "flake-utils",
30
+ "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
31
+ "type": "github"
32
+ },
33
+ "original": {
34
+ "owner": "numtide",
35
+ "repo": "flake-utils",
36
+ "type": "github"
37
+ }
38
+ },
39
+ "myl-discovery": {
40
+ "inputs": {
41
+ "flake-utils": "flake-utils_2",
42
+ "nixpkgs": [
43
+ "nixpkgs"
44
+ ]
45
+ },
46
+ "locked": {
47
+ "lastModified": 1730392848,
48
+ "narHash": "sha256-7RBwu5zJXV+90NBwGZwO459FswogX1zcgsEuOi2Eqlc=",
49
+ "owner": "pschmitt",
50
+ "repo": "myl-discovery",
51
+ "rev": "6cc8eab2e61efbf17ab48356282d35a11b9dd7a4",
52
+ "type": "github"
53
+ },
54
+ "original": {
55
+ "owner": "pschmitt",
56
+ "repo": "myl-discovery",
57
+ "type": "github"
58
+ }
59
+ },
60
+ "nixpkgs": {
61
+ "locked": {
62
+ "lastModified": 1730200266,
63
+ "narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=",
64
+ "owner": "NixOS",
65
+ "repo": "nixpkgs",
66
+ "rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd",
67
+ "type": "github"
68
+ },
69
+ "original": {
70
+ "owner": "NixOS",
71
+ "ref": "nixos-unstable",
72
+ "repo": "nixpkgs",
73
+ "type": "github"
74
+ }
75
+ },
76
+ "root": {
77
+ "inputs": {
78
+ "flake-utils": "flake-utils",
79
+ "myl-discovery": "myl-discovery",
80
+ "nixpkgs": "nixpkgs"
81
+ }
82
+ },
83
+ "systems": {
84
+ "locked": {
85
+ "lastModified": 1681028828,
86
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
87
+ "owner": "nix-systems",
88
+ "repo": "default",
89
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
90
+ "type": "github"
91
+ },
92
+ "original": {
93
+ "owner": "nix-systems",
94
+ "repo": "default",
95
+ "type": "github"
96
+ }
97
+ },
98
+ "systems_2": {
99
+ "locked": {
100
+ "lastModified": 1681028828,
101
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
102
+ "owner": "nix-systems",
103
+ "repo": "default",
104
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
105
+ "type": "github"
106
+ },
107
+ "original": {
108
+ "owner": "nix-systems",
109
+ "repo": "default",
110
+ "type": "github"
111
+ }
112
+ }
113
+ },
114
+ "root": "root",
115
+ "version": 7
116
+ }
myl-0.9.2/flake.nix ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ description = "Flake for myl IMAP CLI client and myl-discovery, compatible with multiple systems";
3
+
4
+ inputs = {
5
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
+ flake-utils.url = "github:numtide/flake-utils";
7
+ myl-discovery = {
8
+ url = "github:pschmitt/myl-discovery";
9
+ inputs.nixpkgs.follows = "nixpkgs";
10
+ };
11
+ };
12
+
13
+ outputs =
14
+ {
15
+ self,
16
+ nixpkgs,
17
+ flake-utils,
18
+ myl-discovery,
19
+ ...
20
+ }:
21
+ flake-utils.lib.eachDefaultSystem (
22
+ system:
23
+ let
24
+ pkgs = nixpkgs.legacyPackages.${system};
25
+
26
+ myl = pkgs.python3Packages.buildPythonApplication {
27
+ pname = "myl";
28
+ version = builtins.readFile ./version.txt;
29
+ pyproject = true;
30
+
31
+ src = ./.;
32
+
33
+ buildInputs = [
34
+ pkgs.python3Packages.setuptools
35
+ pkgs.python3Packages.setuptools-scm
36
+ ];
37
+
38
+ propagatedBuildInputs = with pkgs.python3Packages; [
39
+ html2text
40
+ imap-tools
41
+ myl-discovery.packages.${system}.myl-discovery
42
+ rich
43
+ ];
44
+
45
+ meta = {
46
+ description = "Dead simple IMAP CLI client";
47
+ homepage = "https://pypi.org/project/myl/";
48
+ license = pkgs.lib.licenses.gpl3Only;
49
+ maintainers = with pkgs.lib.maintainers; [ pschmitt ];
50
+ platforms = pkgs.lib.platforms.all;
51
+ };
52
+ };
53
+
54
+ devShell = pkgs.mkShell {
55
+ name = "myl-devshell";
56
+
57
+ buildInputs = [
58
+ pkgs.python3
59
+ pkgs.python3Packages.setuptools
60
+ pkgs.python3Packages.setuptools-scm
61
+ pkgs.python3Packages.html2text
62
+ pkgs.python3Packages.imap-tools
63
+ self.packages.${system}.myl-discovery
64
+ pkgs.python3Packages.rich
65
+ ];
66
+
67
+ # Additional development tools
68
+ nativeBuildInputs = [
69
+ pkgs.gh # GitHub CLI
70
+ pkgs.git
71
+ pkgs.python3Packages.ipython
72
+ pkgs.neovim
73
+ ];
74
+
75
+ # Environment variables and shell hooks
76
+ shellHook = ''
77
+ export PYTHONPATH=${self.packages.${system}.myl}/lib/python3.x/site-packages
78
+ echo -e "\e[34mWelcome to the myl development shell!\e[0m"
79
+ # Activate a virtual environment if desired
80
+ # source .venv/bin/activate
81
+ '';
82
+
83
+ # Optional: Set up a Python virtual environment
84
+ # if you prefer using virtualenv or similar tools
85
+ # you can uncomment and configure the following lines
86
+ # shellHook = ''
87
+ # if [ ! -d .venv ]; then
88
+ # python3 -m venv .venv
89
+ # source .venv/bin/activate
90
+ # pip install --upgrade pip
91
+ # else
92
+ # source .venv/bin/activate
93
+ # fi
94
+ # '';
95
+ };
96
+ in
97
+ {
98
+ # pkgs
99
+ packages.myl = myl;
100
+ defaultPackage = myl;
101
+
102
+ devShells.default = devShell;
103
+ }
104
+ );
105
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: myl
3
- Version: 0.8.7
3
+ Version: 0.9.2
4
4
  Summary: Dead simple IMAP CLI client
5
5
  Author-email: Philipp Schmitt <philipp@schmitt.co>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -678,14 +678,19 @@ License: GNU GENERAL PUBLIC LICENSE
678
678
  Public License instead of this License. But first, please read
679
679
  <https://www.gnu.org/licenses/why-not-lgpl.html>.
680
680
 
681
+ Project-URL: homepage, https://github.com/pschmitt/myl
682
+ Project-URL: documentation, https://github.com/pschmitt/myl/blob/head/readme.md
683
+ Project-URL: repository, https://github.com/pschmitt/myl
684
+ Project-URL: issues, https://github.com/pschmitt/myl/issues
681
685
  Keywords: imap,email
682
686
  Classifier: Programming Language :: Python :: 3
683
687
  Requires-Python: >=3.8
684
688
  Description-Content-Type: text/markdown
685
689
  License-File: LICENSE
686
- Requires-Dist: imap-tools==1.5.0
687
- Requires-Dist: myl-discovery==0.5.7
688
- Requires-Dist: rich>=13.7.0
690
+ Requires-Dist: imap-tools<2.0.0,>=1.5.0
691
+ Requires-Dist: myl-discovery>=0.6.1.dev0
692
+ Requires-Dist: rich<14.0.0,>=13.0.0
693
+ Requires-Dist: html2text>=2024.2.26
689
694
 
690
695
  # 📧 myl
691
696
 
@@ -716,15 +721,23 @@ straightforward way to interact with IMAP servers.
716
721
 
717
722
  To install myl, follow these steps:
718
723
 
719
- ```bash
724
+ ```shell
720
725
  pipx install myl
726
+ # or:
727
+ pip install --user myl
728
+ ```
729
+
730
+ on nix you can do this:
731
+
732
+ ```shell
733
+ nix run github:pschmitt/myl -- --help
721
734
  ```
722
735
 
723
736
  ## 🛠️ Usage
724
737
 
725
738
  Here's how you can use myl:
726
739
 
727
- ```bash
740
+ ```shell
728
741
  myl --help
729
742
  ```
730
743
 
@@ -732,7 +745,7 @@ This command will display the help information for the `myl` command.
732
745
 
733
746
  Here are some examples of using flags with the `myl` command:
734
747
 
735
- ```bash
748
+ ```shell
736
749
  # Connect to an IMAP server
737
750
  myl --server imap.example.com --port 143 --starttls --username "$username" --password "$password"
738
751
 
@@ -1,8 +1,11 @@
1
1
  .gitignore
2
2
  LICENSE
3
3
  README.md
4
+ flake.lock
5
+ flake.nix
4
6
  myl.py
5
7
  pyproject.toml
8
+ version.txt
6
9
  .github/dependabot.yml
7
10
  .github/workflows/lint.yaml
8
11
  .github/workflows/pypi.yaml
@@ -0,0 +1,4 @@
1
+ imap-tools<2.0.0,>=1.5.0
2
+ myl-discovery>=0.6.1.dev0
3
+ rich<14.0.0,>=13.0.0
4
+ html2text>=2024.2.26
myl-0.9.2/myl.py ADDED
@@ -0,0 +1,508 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ from importlib.metadata import version, PackageNotFoundError
5
+ import argparse
6
+ import logging
7
+ import ssl
8
+ import sys
9
+ from json import dumps as json_dumps
10
+
11
+ import html2text
12
+ from imap_tools.consts import MailMessageFlags
13
+ from imap_tools.mailbox import (
14
+ BaseMailBox,
15
+ MailBox,
16
+ MailBoxTls,
17
+ MailBoxUnencrypted,
18
+ )
19
+ from imap_tools.query import AND
20
+ from myldiscovery import autodiscover
21
+ from rich import print, print_json
22
+ from rich.console import Console
23
+ from rich.logging import RichHandler
24
+ from rich.table import Table
25
+
26
+ try:
27
+ __version__ = version("myl")
28
+ except PackageNotFoundError:
29
+ pass
30
+
31
+ LOGGER = logging.getLogger(__name__)
32
+ IMAP_PORT = 993
33
+ GMAIL_IMAP_SERVER = "imap.gmail.com"
34
+ GMAIL_IMAP_PORT = IMAP_PORT
35
+ GMAIL_SENT_FOLDER = "[Gmail]/Sent Mail"
36
+ # GMAIL_ALL_FOLDER = "[Gmail]/All Mail"
37
+
38
+
39
+ class MissingServerException(Exception):
40
+ pass
41
+
42
+
43
+ def error_msg(msg):
44
+ print(f"[red]{msg}[/red]", file=sys.stderr)
45
+
46
+
47
+ def mail_to_dict(msg, date_format="%Y-%m-%d %H:%M:%S"):
48
+ return {
49
+ "uid": msg.uid,
50
+ "subject": msg.subject,
51
+ "from": msg.from_,
52
+ "to": msg.to,
53
+ "date": msg.date.strftime(date_format),
54
+ "timestamp": str(int(msg.date.timestamp())),
55
+ "unread": mail_is_unread(msg),
56
+ "flags": msg.flags,
57
+ "content": {
58
+ "raw": msg.obj.as_string(),
59
+ "html": msg.html,
60
+ "text": msg.text,
61
+ },
62
+ "attachments": msg.attachments,
63
+ }
64
+
65
+
66
+ def mail_to_json(msg, date_format="%Y-%m-%d %H:%M:%S"):
67
+ return json_dumps(mail_to_dict(msg, date_format))
68
+
69
+
70
+ def mail_is_unread(msg):
71
+ return MailMessageFlags.SEEN not in msg.flags
72
+
73
+
74
+ def parse_args():
75
+ parser = argparse.ArgumentParser()
76
+ subparsers = parser.add_subparsers(
77
+ dest="command", help="Available commands"
78
+ )
79
+ parser.add_argument(
80
+ "-V",
81
+ "--version",
82
+ action="version",
83
+ version=f"%(prog)s {__version__}",
84
+ )
85
+
86
+ # Default command: list all emails
87
+ subparsers.add_parser("list", help="List all emails")
88
+
89
+ # Get/show email command
90
+ get_parser = subparsers.add_parser(
91
+ "get", help="Retrieve a specific email or attachment"
92
+ )
93
+ get_parser.add_argument("MAILID", help="Mail ID to fetch", type=int)
94
+ get_parser.add_argument(
95
+ "ATTACHMENT",
96
+ help="Name of the attachment to fetch",
97
+ nargs="?",
98
+ default=None,
99
+ )
100
+
101
+ # Delete email command
102
+ delete_parser = subparsers.add_parser("delete", help="Delete an email")
103
+ delete_parser.add_argument(
104
+ "MAILIDS", help="Mail ID(s) to delete", type=int, nargs="+"
105
+ )
106
+
107
+ # Mark email as read/unread
108
+ mark_read_parser = subparsers.add_parser(
109
+ "read", help="mark an email as read"
110
+ )
111
+ mark_read_parser.add_argument(
112
+ "MAILIDS", help="Mail ID(s) to mark as read", type=int, nargs="+"
113
+ )
114
+ mark_unread_parser = subparsers.add_parser(
115
+ "unread", help="mark an email as unread"
116
+ )
117
+ mark_unread_parser.add_argument(
118
+ "MAILIDS", help="Mail ID(s) to mark as unread", type=int, nargs="+"
119
+ )
120
+
121
+ # Optional arguments
122
+ parser.add_argument(
123
+ "-d", "--debug", help="Enable debug mode", action="store_true"
124
+ )
125
+
126
+ # IMAP connection settings
127
+ parser.add_argument(
128
+ "-s", "--server", help="IMAP server address", required=False
129
+ )
130
+ parser.add_argument(
131
+ "--google",
132
+ "--gmail",
133
+ help="Use Google IMAP settings (overrides --port, --server etc.)",
134
+ action="store_true",
135
+ default=False,
136
+ )
137
+ parser.add_argument(
138
+ "-a",
139
+ "--auto",
140
+ help="Autodiscovery of the required server and port",
141
+ action="store_true",
142
+ default=True,
143
+ )
144
+ parser.add_argument(
145
+ "-P", "--port", help="IMAP server port", default=IMAP_PORT
146
+ )
147
+ parser.add_argument("--ssl", help="SSL", action="store_true", default=True)
148
+ parser.add_argument(
149
+ "--starttls", help="STARTTLS", action="store_true", default=False
150
+ )
151
+ parser.add_argument(
152
+ "--insecure",
153
+ help="Disable cert validation",
154
+ action="store_true",
155
+ default=False,
156
+ )
157
+
158
+ # Credentials
159
+ parser.add_argument(
160
+ "-u", "--username", help="IMAP username", required=True
161
+ )
162
+ password_group = parser.add_mutually_exclusive_group(required=True)
163
+ password_group.add_argument("-p", "--password", help="IMAP password")
164
+ password_group.add_argument(
165
+ "--password-file",
166
+ help="IMAP password (file path)",
167
+ type=argparse.FileType("r"),
168
+ )
169
+
170
+ # Display preferences
171
+ parser.add_argument(
172
+ "-c",
173
+ "--count",
174
+ help="Number of messages to fetch",
175
+ default=10,
176
+ type=int,
177
+ )
178
+ parser.add_argument(
179
+ "-t", "--no-title", help="Do not show title", action="store_true"
180
+ )
181
+ parser.add_argument(
182
+ "--date-format", help="Date format", default="%H:%M %d/%m/%Y"
183
+ )
184
+
185
+ # IMAP actions
186
+ parser.add_argument(
187
+ "-m",
188
+ "--mark-seen",
189
+ help="Mark seen",
190
+ action="store_true",
191
+ default=False,
192
+ )
193
+
194
+ # Email filtering
195
+ parser.add_argument("-f", "--folder", help="IMAP folder", default="INBOX")
196
+ parser.add_argument(
197
+ "--sent",
198
+ help="Sent email",
199
+ action="store_true",
200
+ )
201
+ parser.add_argument("-S", "--search", help="Search string", default="ALL")
202
+ parser.add_argument(
203
+ "--unread",
204
+ help="Limit to unread emails",
205
+ action="store_true",
206
+ default=False,
207
+ )
208
+
209
+ # Output preferences
210
+ parser.add_argument(
211
+ "-H",
212
+ "--html",
213
+ help="Show HTML email",
214
+ action="store_true",
215
+ default=False,
216
+ )
217
+ parser.add_argument(
218
+ "-j",
219
+ "--json",
220
+ help="JSON output",
221
+ action="store_true",
222
+ default=False,
223
+ )
224
+ parser.add_argument(
225
+ "-r",
226
+ "--raw",
227
+ help="Show the raw email",
228
+ action="store_true",
229
+ default=False,
230
+ )
231
+
232
+ return parser.parse_args()
233
+
234
+
235
+ def mb_connect(console, args) -> BaseMailBox:
236
+ imap_password = args.password or (
237
+ args.password_file and args.password_file.read()
238
+ )
239
+
240
+ if args.google:
241
+ args.server = GMAIL_IMAP_SERVER
242
+ args.port = GMAIL_IMAP_PORT
243
+ args.starttls = False
244
+
245
+ if args.sent or args.folder == "Sent":
246
+ args.folder = GMAIL_SENT_FOLDER
247
+ # elif args.folder == "INBOX":
248
+ # args.folder = GMAIL_ALL_FOLDER
249
+ else:
250
+ if args.auto:
251
+ try:
252
+ settings = autodiscover(
253
+ args.username,
254
+ password=imap_password,
255
+ insecure=args.insecure,
256
+ ).get("imap", {})
257
+ except Exception:
258
+ error_msg("Failed to autodiscover IMAP settings")
259
+ if args.debug:
260
+ console.print_exception(show_locals=True)
261
+ raise
262
+
263
+ LOGGER.debug(f"Discovered settings: {settings})")
264
+ args.server = settings.get("server")
265
+ args.port = settings.get("port", IMAP_PORT)
266
+ args.starttls = settings.get("starttls")
267
+ args.ssl = settings.get("ssl")
268
+
269
+ if args.sent:
270
+ args.folder = "Sent"
271
+
272
+ if not args.server:
273
+ error_msg(
274
+ "No server specified\n"
275
+ "You need to either:\n"
276
+ "- specify a server using --server HOSTNAME\n"
277
+ "- set --google if you are using a Gmail account\n"
278
+ "- use --auto to attempt autodiscovery"
279
+ )
280
+ raise MissingServerException()
281
+
282
+ ssl_context = ssl.create_default_context()
283
+ if args.insecure:
284
+ ssl_context.check_hostname = False
285
+ ssl_context.verify_mode = ssl.CERT_NONE
286
+
287
+ mb_kwargs = {"host": args.server, "port": args.port}
288
+ if args.ssl:
289
+ mb = MailBox
290
+ mb_kwargs["ssl_context"] = ssl_context
291
+ elif args.starttls:
292
+ mb = MailBoxTls
293
+ mb_kwargs["ssl_context"] = ssl_context
294
+ else:
295
+ mb = MailBoxUnencrypted
296
+
297
+ mailbox = mb(**mb_kwargs)
298
+ mailbox.login(args.username, imap_password, args.folder)
299
+ return mailbox
300
+
301
+
302
+ def display_single_mail(
303
+ mailbox: BaseMailBox,
304
+ mail_id: int,
305
+ attachment: str | None = None,
306
+ mark_seen: bool = False,
307
+ raw: bool = False,
308
+ html: bool = False,
309
+ json: bool = False,
310
+ ):
311
+ LOGGER.debug("Fetch mail %s", mail_id)
312
+ msg = next(mailbox.fetch(f"UID {mail_id}", mark_seen=mark_seen))
313
+ LOGGER.debug("Fetched mail %s", msg)
314
+
315
+ if attachment:
316
+ for att in msg.attachments:
317
+ if att.filename == attachment:
318
+ sys.stdout.buffer.write(att.payload)
319
+ return 0
320
+ print(
321
+ f"attachment {attachment} not found",
322
+ file=sys.stderr,
323
+ )
324
+ return 1
325
+
326
+ if html:
327
+ output = msg.text
328
+ if raw:
329
+ output = msg.html
330
+ else:
331
+ output = html2text.html2text(msg.html)
332
+ print(output)
333
+ elif raw:
334
+ print(msg.obj.as_string())
335
+ return 0
336
+ elif json:
337
+ print_json(mail_to_json(msg))
338
+ return 0
339
+ else:
340
+ print(msg.text)
341
+
342
+ for att in msg.attachments:
343
+ print(f"📎 Attachment: {att.filename}", file=sys.stderr)
344
+ return 0
345
+
346
+
347
+ def display_emails(
348
+ mailbox,
349
+ console,
350
+ no_title=False,
351
+ search="ALL",
352
+ unread_only=False,
353
+ count=10,
354
+ mark_seen=False,
355
+ json=False,
356
+ date_format="%H:%M %d/%m/%Y",
357
+ ):
358
+ json_data = []
359
+ table = Table(
360
+ show_header=not no_title,
361
+ header_style="bold",
362
+ expand=True,
363
+ show_lines=False,
364
+ show_edge=False,
365
+ pad_edge=False,
366
+ box=None,
367
+ row_styles=["", "dim"],
368
+ )
369
+ table.add_column("ID", style="red", no_wrap=True)
370
+ table.add_column("Subject", style="green", no_wrap=True, ratio=3)
371
+ table.add_column("From", style="blue", no_wrap=True, ratio=2)
372
+ table.add_column("Date", style="cyan", no_wrap=True)
373
+
374
+ if unread_only:
375
+ search = AND(seen=False)
376
+
377
+ for msg in mailbox.fetch(
378
+ criteria=search,
379
+ reverse=True,
380
+ bulk=True,
381
+ limit=count,
382
+ mark_seen=mark_seen,
383
+ headers_only=False, # required for attachments
384
+ ):
385
+ subj_prefix = "🆕 " if mail_is_unread(msg) else ""
386
+ subj_prefix += "📎 " if len(msg.attachments) > 0 else ""
387
+ subject = (
388
+ msg.subject.replace("\n", "") if msg.subject else "<no-subject>"
389
+ )
390
+ if json:
391
+ json_data.append(mail_to_dict(msg))
392
+ else:
393
+ table.add_row(
394
+ msg.uid if msg.uid else "???",
395
+ f"{subj_prefix}{subject}",
396
+ msg.from_,
397
+ (msg.date.strftime(date_format) if msg.date else "???"),
398
+ )
399
+ if table.row_count >= count:
400
+ break
401
+
402
+ if json:
403
+ print_json(json_dumps(json_data))
404
+ else:
405
+ console.print(table)
406
+ if table.row_count == 0:
407
+ print(
408
+ "[yellow italic]No messages[/yellow italic]",
409
+ file=sys.stderr,
410
+ )
411
+ return 0
412
+
413
+
414
+ def delete_emails(mailbox: BaseMailBox, mail_ids: list):
415
+ LOGGER.warning("Deleting mails %s", mail_ids)
416
+ mailbox.delete([str(x) for x in mail_ids])
417
+ return 0
418
+
419
+
420
+ def set_seen(mailbox: BaseMailBox, mail_ids: list, value=True):
421
+ LOGGER.info(
422
+ "Marking mails as %s: %s", "read" if value else "unread", mail_ids
423
+ )
424
+ mailbox.flag(
425
+ [str(x) for x in mail_ids],
426
+ flag_set=(MailMessageFlags.SEEN),
427
+ value=value,
428
+ )
429
+ return 0
430
+
431
+
432
+ def mark_read(mailbox: BaseMailBox, mail_ids: list):
433
+ return set_seen(mailbox, mail_ids, value=True)
434
+
435
+
436
+ def mark_unread(mailbox: BaseMailBox, mail_ids: list):
437
+ return set_seen(mailbox, mail_ids, value=False)
438
+
439
+
440
+ def main() -> int:
441
+ console = Console()
442
+ args = parse_args()
443
+ logging.basicConfig(
444
+ format="%(message)s",
445
+ handlers=[RichHandler(console=console)],
446
+ level=logging.DEBUG if args.debug else logging.INFO,
447
+ )
448
+ LOGGER.debug(args)
449
+
450
+ try:
451
+ with mb_connect(console, args) as mailbox:
452
+ # inbox display
453
+ if args.command in ["list", None]:
454
+ return display_emails(
455
+ mailbox=mailbox,
456
+ console=console,
457
+ no_title=args.no_title,
458
+ search=args.search,
459
+ unread_only=args.unread,
460
+ count=args.count,
461
+ mark_seen=args.mark_seen,
462
+ json=args.json,
463
+ date_format=args.date_format,
464
+ )
465
+
466
+ # single email
467
+ # FIXME $ myl 219 raises an argparse error
468
+ elif args.command in ["get", "show", "display"]:
469
+ return display_single_mail(
470
+ mailbox=mailbox,
471
+ mail_id=args.MAILID,
472
+ attachment=args.ATTACHMENT,
473
+ mark_seen=args.mark_seen,
474
+ raw=args.raw,
475
+ html=args.html,
476
+ json=args.json,
477
+ )
478
+
479
+ # mark emails as read
480
+ elif args.command in ["read"]:
481
+ return mark_read(
482
+ mailbox=mailbox,
483
+ mail_ids=args.MAILIDS,
484
+ )
485
+
486
+ elif args.command in ["unread"]:
487
+ return mark_unread(
488
+ mailbox=mailbox,
489
+ mail_ids=args.MAILIDS,
490
+ )
491
+
492
+ # delete email
493
+ elif args.command in ["delete", "remove"]:
494
+ return delete_emails(
495
+ mailbox=mailbox,
496
+ mail_ids=args.MAILIDS,
497
+ )
498
+ else:
499
+ error_msg(f"Unknown command: {args.command}")
500
+ return 1
501
+
502
+ except Exception:
503
+ console.print_exception(show_locals=True)
504
+ return 1
505
+
506
+
507
+ if __name__ == "__main__":
508
+ sys.exit(main())
@@ -16,11 +16,23 @@ classifiers = [
16
16
  "Programming Language :: Python :: 3",
17
17
  ]
18
18
  dependencies = [
19
- "imap-tools == 1.5.0",
20
- "myl-discovery == 0.5.7",
21
- "rich >= 13.7.0",
19
+ "imap-tools >= 1.5.0, < 2.0.0",
20
+ "myl-discovery >= 0.6.1.dev0",
21
+ "rich >= 13.0.0, <14.0.0",
22
+ "html2text >= 2024.2.26"
22
23
  ]
23
- version = "0.8.7"
24
+ dynamic = ["version"]
25
+
26
+ [tool.setuptools_scm]
27
+ version_file = "version.txt"
28
+ version_scheme = "only-version"
29
+ local_scheme = "no-local-version"
30
+
31
+ [project.urls]
32
+ homepage = "https://github.com/pschmitt/myl"
33
+ documentation = "https://github.com/pschmitt/myl/blob/head/readme.md"
34
+ repository = "https://github.com/pschmitt/myl"
35
+ issues = "https://github.com/pschmitt/myl/issues"
24
36
 
25
37
  [tool.black]
26
38
  line-length = 79
myl-0.9.2/version.txt ADDED
@@ -0,0 +1 @@
1
+ 0.9.2
@@ -1,3 +0,0 @@
1
- imap-tools==1.5.0
2
- myl-discovery==0.5.7
3
- rich>=13.7.0
myl-0.8.7/myl.py DELETED
@@ -1,215 +0,0 @@
1
- import argparse
2
- import logging
3
- import sys
4
-
5
- import imap_tools
6
- from myldiscovery import autodiscover
7
- from rich import print
8
- from rich.console import Console
9
- from rich.logging import RichHandler
10
- from rich.table import Table
11
-
12
- LOGGER = logging.getLogger(__name__)
13
- IMAP_PORT = 993
14
- GMAIL_IMAP_SERVER = "imap.gmail.com"
15
- GMAIL_IMAP_PORT = IMAP_PORT
16
- GMAIL_SENT_FOLDER = "[Gmail]/Sent Mail"
17
- # GMAIL_ALL_FOLDER = "[Gmail]/All Mail"
18
-
19
-
20
- def error_msg(msg):
21
- print(f"[red]{msg}[/red]", file=sys.stderr)
22
-
23
-
24
- def parse_args():
25
- parser = argparse.ArgumentParser()
26
- parser.add_argument("-d", "--debug", help="Debug", action="store_true")
27
- parser.add_argument(
28
- "-s", "--server", help="IMAP server address", required=False
29
- )
30
- parser.add_argument(
31
- "--google",
32
- "--gmail",
33
- help="Use Google IMAP settings (overrides --port, --server etc.)",
34
- action="store_true",
35
- default=False,
36
- )
37
- parser.add_argument(
38
- "-a",
39
- "--auto",
40
- help="Autodiscovery of the required server and port",
41
- action="store_true",
42
- default=False,
43
- )
44
- parser.add_argument(
45
- "-P", "--port", help="IMAP server port", default=IMAP_PORT
46
- )
47
- parser.add_argument(
48
- "--starttls", help="Start TLS", action="store_true", default=False
49
- )
50
- parser.add_argument(
51
- "-c",
52
- "--count",
53
- help="Number of messages to fetch",
54
- default=10,
55
- type=int,
56
- )
57
- parser.add_argument(
58
- "-m", "--mark-seen", help="Mark seen", action="store_true"
59
- )
60
- parser.add_argument(
61
- "-u", "--username", help="IMAP username", required=True
62
- )
63
- parser.add_argument(
64
- "-p", "--password", help="IMAP password", required=True
65
- )
66
- parser.add_argument(
67
- "-t", "--no-title", help="Do not show title", action="store_true"
68
- )
69
- parser.add_argument("-f", "--folder", help="IMAP folder", default="INBOX")
70
- parser.add_argument(
71
- "--sent",
72
- help="Sent email",
73
- action="store_true",
74
- )
75
- parser.add_argument("-S", "--search", help="Search string", default="ALL")
76
- parser.add_argument("-w", "--wrap", help="Wrap text", action="store_true")
77
- parser.add_argument("-H", "--html", help="Show HTML", action="store_true")
78
- parser.add_argument(
79
- "-r",
80
- "--raw",
81
- help="Show the raw email",
82
- action="store_true",
83
- default=False,
84
- )
85
- parser.add_argument("MAILID", help="Mail ID to fetch", nargs="?")
86
- parser.add_argument(
87
- "ATTACHMENT", help="Name of the attachment to fetch", nargs="?"
88
- )
89
-
90
- return parser.parse_args()
91
-
92
-
93
- def main():
94
- console = Console()
95
- args = parse_args()
96
- logging.basicConfig(
97
- format="%(message)s",
98
- handlers=[RichHandler(console=console)],
99
- level=logging.DEBUG if args.debug else logging.INFO,
100
- )
101
- LOGGER.debug(args)
102
-
103
- if args.google:
104
- args.server = GMAIL_IMAP_SERVER
105
- args.port = GMAIL_IMAP_PORT
106
- args.starttls = False
107
-
108
- if args.sent or args.folder == "Sent":
109
- args.folder = GMAIL_SENT_FOLDER
110
- # elif args.folder == "INBOX":
111
- # args.folder = GMAIL_ALL_FOLDER
112
- else:
113
- if args.auto:
114
- try:
115
- settings = autodiscover(
116
- args.username, password=args.password
117
- ).get("imap")
118
- except Exception:
119
- error_msg("Failed to autodiscover IMAP settings")
120
- if args.debug:
121
- console.print_exception(show_locals=True)
122
- return 1
123
- LOGGER.debug(f"Discovered settings: {settings})")
124
- args.server = settings.get("server")
125
- args.port = settings.get("port", IMAP_PORT)
126
- args.starttls = settings.get("starttls")
127
-
128
- if args.sent:
129
- args.folder = "Sent"
130
-
131
- if not args.server:
132
- error_msg(
133
- "No server specified\n"
134
- "You need to either:\n"
135
- "- specify a server using --server HOSTNAME\n"
136
- "- set --google if you are using a Gmail account\n"
137
- "- use --auto to attempt autodiscovery"
138
- )
139
- return 2
140
-
141
- table = Table(
142
- expand=True,
143
- show_header=not args.no_title,
144
- header_style="bold",
145
- show_lines=False,
146
- box=None,
147
- )
148
- table.add_column("ID", style="red", no_wrap=not args.wrap, max_width=10)
149
- table.add_column(
150
- "Subject", style="green", no_wrap=not args.wrap, max_width=30
151
- )
152
- table.add_column("From", style="blue", no_wrap=not args.wrap, max_width=30)
153
- table.add_column("Date", style="cyan", no_wrap=not args.wrap)
154
-
155
- mb = imap_tools.MailBoxTls if args.starttls else imap_tools.MailBox
156
-
157
- try:
158
- with mb(args.server, port=args.port).login(
159
- args.username, args.password, args.folder
160
- ) as mailbox:
161
- if args.MAILID:
162
- msg = next(
163
- mailbox.fetch(
164
- f"UID {args.MAILID}", mark_seen=args.mark_seen
165
- )
166
- )
167
- if args.ATTACHMENT:
168
- for att in msg.attachments:
169
- if att.filename == args.ATTACHMENT:
170
- sys.stdout.buffer.write(att.payload)
171
- return 0
172
- print(
173
- f"Attachment {args.ATTACHMENT} not found",
174
- file=sys.stderr,
175
- )
176
- return 1
177
- else:
178
- if args.raw:
179
- print(msg.obj.as_string())
180
- return 0
181
- print(msg.text if not args.html else msg.html)
182
- for att in msg.attachments:
183
- print(
184
- f"📎 Attachment: {att.filename}", file=sys.stderr
185
- )
186
- return 0
187
-
188
- for msg in mailbox.fetch(
189
- criteria=args.search,
190
- reverse=True,
191
- bulk=True,
192
- limit=args.count,
193
- mark_seen=args.mark_seen,
194
- headers_only=False, # required for attachments
195
- ):
196
- subj_prefix = "📎 " if len(msg.attachments) > 0 else ""
197
- table.add_row(
198
- msg.uid if msg.uid else "???",
199
- subj_prefix
200
- + (msg.subject if msg.subject else "<no-subject>"),
201
- msg.from_,
202
- msg.date.strftime("%H:%M %d/%m/%Y") if msg.date else "???",
203
- )
204
- if len(table.rows) >= args.count:
205
- break
206
-
207
- console.print(table)
208
- return 0
209
- except Exception:
210
- console.print_exception(show_locals=True)
211
- return 1
212
-
213
-
214
- if __name__ == "__main__":
215
- sys.exit(main())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes