2525"""
2626
2727import argparse
28+ import glob
2829import json
2930import os
3031import subprocess
@@ -60,13 +61,20 @@ def server_dir() -> str:
6061 return f"{ host } _{ parsed .port } " if parsed .port else host
6162
6263
63- def account_file (email : str ) -> Optional [str ]:
64- path = os .path .join (LEGO_PATH , "accounts" , server_dir (), email , "account.json" )
65- return path if os .path .isfile (path ) else None
64+ def account_file () -> Optional [str ]:
65+ """Find the account document by globbing rather than by building the path.
6666
67+ lego names the directory after the account email, and after a placeholder of
68+ its own choosing when there is no email. Globbing means we neither have to
69+ know that placeholder nor track it across lego versions.
70+ """
71+ pattern = os .path .join (LEGO_PATH , "accounts" , server_dir (), "*" , "account.json" )
72+ matches = sorted (glob .glob (pattern ))
73+ return matches [0 ] if matches else None
6774
68- def account_uri (email : str ) -> Optional [str ]:
69- path = account_file (email )
75+
76+ def account_uri () -> Optional [str ]:
77+ path = account_file ()
7078 if not path :
7179 return None
7280 try :
@@ -96,15 +104,14 @@ def _renewed_marker(domain: str) -> str:
96104
97105
98106def _common_flags (email : str ) -> List [str ]:
99- return [
100- "--path" ,
101- LEGO_PATH ,
102- "--server" ,
103- acme_server (),
104- "--email" ,
105- email ,
106- "--accept-tos" ,
107- ]
107+ flags = ["--path" , LEGO_PATH , "--server" , acme_server (), "--accept-tos" ]
108+ # The ACME contact address is optional (RFC 8555 §7.3), and Let's Encrypt
109+ # stopped sending expiry notifications in 2025, so it buys little. It is
110+ # also published: the account document is served as evidence, so an address
111+ # set here becomes public. Omit it unless the operator wants it.
112+ if email :
113+ flags += ["--email" , email ]
114+ return flags
108115
109116
110117def _run (cmd : List [str ], timeout : int = RUN_TIMEOUT ) -> Tuple [int , str ]:
@@ -126,7 +133,7 @@ def _run(cmd: List[str], timeout: int = RUN_TIMEOUT) -> Tuple[int, str]:
126133
127134def register (email : str ) -> int :
128135 """Create the ACME account without issuing anything."""
129- if account_file (email ):
136+ if account_file ():
130137 print ("ACME account already exists" )
131138 return EXIT_UNCHANGED
132139
@@ -135,7 +142,7 @@ def register(email: str) -> int:
135142 [LEGO_BIN , "accounts" , "register" ] + _common_flags (email ),
136143 timeout = REGISTER_TIMEOUT ,
137144 )
138- if code == 0 and account_file (email ):
145+ if code == 0 and account_file ():
139146 print ("✓ ACME account registered" )
140147 return EXIT_CHANGED
141148 print (f"✗ ACME account registration failed (exit code { code } )" , file = sys .stderr )
@@ -214,10 +221,7 @@ def main() -> int:
214221 email = args .email or os .environ .get ("ACME_EMAIL" ) or os .environ .get ("CERTBOT_EMAIL" , "" )
215222
216223 if args .action == "account-uri" :
217- if not email :
218- print ("Error: --email or ACME_EMAIL is required" , file = sys .stderr )
219- return EXIT_ERROR
220- uri = account_uri (email )
224+ uri = account_uri ()
221225 if not uri :
222226 return EXIT_ERROR
223227 print (uri )
@@ -231,9 +235,6 @@ def main() -> int:
231235 print (f"{ crt } { key } " )
232236 return EXIT_CHANGED
233237
234- if not email :
235- print ("Error: --email or ACME_EMAIL is required" , file = sys .stderr )
236- return EXIT_ERROR
237238 if not os .path .isfile (LEGO_BIN ):
238239 print (f"Error: lego binary not found at { LEGO_BIN } " , file = sys .stderr )
239240 return EXIT_ERROR
0 commit comments