iRedMail 批量创建邮箱工具 - Windows 可执行 .exe 文件
在 Windows 系统里,通过鼠标双击 .exe 程序,连接远端的 iRedMail 邮局服务器,批量创建邮箱地址。
使用说明:
- 先确保 iRedMail 邮局服务器正常运行
- 确认邮局管理员登录信息有效
- 在自己的 Windows 系统里,运行 .exe 批量创建工具
在 Windows 系统里,通过鼠标双击 .exe 程序,连接远端的 iRedMail 邮局服务器,批量创建邮箱地址。
使用说明:
环境:debian 12.10 ,iRedMail - 1.7.3
目标:批量配置域名 dkim
步骤:
如需手工操作:
编辑 /etc/amavis/conf.d/50-user 文件内容,将签名的邮件域名和文件等追加进去
# Add dkim_key here.
dkim_key('iredmail.demo.anqun.org', 'dkim', '/var/lib/dkim/iredmail.demo.anqun.org.pem');
@dkim_signature_options_bysender_maps = ({
# 'd' defaults to a domain of an author/sender address,
# 's' defaults to whatever selector is offered by a matching key
# Per-domain dkim key
#"domain.com" => { d => "domain.com", a => 'rsa-sha256', ttl => 10*24*3600 },
# catch-all (one dkim key for all domains)
'.' => {d => 'iredmail.demo.anqun.org',
a => 'rsa-sha256',
c => 'relaxed/simple',
ttl => 30*24*3600 },
});
dkim_setup.py 的文件内容:
#!/usr/bin/env python3
import subprocess
import os
import datetime
import json
AMAVIS_CONFIG_FILE = "/etc/amavis/conf.d/50-user"
DKIM_BASE_PATH = "/var/lib/dkim"
DKIM_KEY_SIZE = 2048 # Or 2048, if desired
AMAVIS_COMMAND = "amavisd" # Changed from amavisd-new
DNS_OUTPUT_FILE = "dkim_dns_records.json"
def generate_dkim_key(domain):
"""Generates a DKIM key for the given domain."""
key_path = os.path.join(DKIM_BASE_PATH, f"{domain}.pem")
command = [AMAVIS_COMMAND, "genrsa", key_path, str(DKIM_KEY_SIZE)]
try:
subprocess.run(command, check=True)
subprocess.run(["chown", "amavis:amavis", key_path], check=True)
subprocess.run(["chmod", "0400", key_path], check=True)
print(f"DKIM key generated for {domain} at {key_path}")
return key_path
except subprocess.CalledProcessError as e:
print(f"Error generating DKIM key for {domain}: {e}")
return None
def get_dkim_public_key(key_path):
"""Extracts the DKIM public key from the .pem file using openssl."""
try:
command = ["openssl", "rsa", "-in", key_path, "-pubout", "-outform", "PEM"]
process = subprocess.run(command, capture_output=True, text=True, check=True)
public_key = process.stdout.strip()
# Remove the BEGIN and END PUBLIC KEY lines and any newlines
public_key = public_key.replace("-----BEGIN PUBLIC KEY-----", "")
public_key = public_key.replace("-----END PUBLIC KEY-----", "")
public_key = public_key.replace("\n", "")
return public_key
except subprocess.CalledProcessError as e:
print(f"Error extracting DKIM public key using openssl: {e}")
print(f"Stderr: {e.stderr}") # Print stderr for more details
return None
except Exception as e:
print(f"Error extracting DKIM public key: {e}")
return None
def domain_exists(domain):
"""Checks if the domain is already configured in Amavis."""
try:
with open(AMAVIS_CONFIG_FILE, "r") as f:
config_content = f.read()
return domain in config_content
except FileNotFoundError:
print(f"Error: Amavis config file not found at {AMAVIS_CONFIG_FILE}")
return False
def backup_amavis_config():
"""Backs up the Amavis configuration file with a timestamp."""
timestamp = datetime.datetime.now().strftime("%Y.%m.%d.%H.%M.%S")
backup_file = f"{AMAVIS_CONFIG_FILE}.{timestamp}"
try:
subprocess.run(["cp", AMAVIS_CONFIG_FILE, backup_file], check=True)
print(f"Amavis config backed up to {backup_file}")
return True
except subprocess.CalledProcessError as e:
print(f"Error backing up Amavis config: {e}")
return False
def update_amavis_config(domain, key_path):
"""Updates the Amavis configuration file with the DKIM settings."""
try:
with open(AMAVIS_CONFIG_FILE, "r") as f:
config_lines = f.readlines()
dkim_key_insert_point = None
start_index = None
end_index = None
last_entry_index = None
for i, line in enumerate(config_lines):
if "dkim_key(" in line:
dkim_key_insert_point = i + 1
if "@dkim_signature_options_bysender_maps = (" in line:
start_index = i
if start_index is not None and "=>" in line:
# 检查是否是一个 domain entry 的开始
if line.lstrip().startswith('"') or line.lstrip().startswith("'"):
last_entry_index = i
if "});" in line and start_index is not None:
end_index = i
break
if dkim_key_insert_point is None:
print("Could not find insertion point for dkim_key.")
return False
if start_index is None or end_index is None:
print("Could not find signature options structure.")
return False
# 如果没有找到任何已有的 domain entry,则默认插入在 start_index + 1
insert_sig_index = last_entry_index + 1 if last_entry_index is not None else start_index + 1
# 构造新行
new_dkim_key_line = f"dkim_key('{domain}', 'dkim', '{key_path}');\n"
new_dkim_sig_line = f' "{domain}" => {{ d => "{domain}", a => \'rsa-sha256\', ttl => 10*24*3600 }},\n'
# 插入 dkim_key
config_lines.insert(dkim_key_insert_point, new_dkim_key_line)
# 如果不是第一个条目,检查前一行是否有逗号
if last_entry_index is not None:
prev_line = config_lines[insert_sig_index - 1]
if not prev_line.strip().endswith(','):
config_lines[insert_sig_index - 1] = prev_line.rstrip('\n') + ',\n'
# 插入新的签名选项
config_lines.insert(insert_sig_index, new_dkim_sig_line)
# 写回文件
with open(AMAVIS_CONFIG_FILE, "w") as f:
f.writelines(config_lines)
print(f"Amavis config updated for {domain}")
return True
except FileNotFoundError:
print(f"Error: Amavis config file not found at {AMAVIS_CONFIG_FILE}")
return False
except Exception as e:
print(f"Error updating Amavis config: {e}")
return False
def restart_amavis():
"""Restarts the Amavis service."""
try:
subprocess.run(["systemctl", "restart", "amavis"], check=True) # Corrected command
print("Amavis service restarted.")
return True
except subprocess.CalledProcessError as e:
print(f"Error restarting Amavis: {e}")
return False
def write_dns_record(domain, public_key):
"""Writes the DNS record to a JSON file."""
dns_record_name = f"dkim._domainkey.{domain}"
# Construct the TXT record value
txt_record_value = f"v=DKIM1; p={public_key}"
data = {dns_record_name: txt_record_value}
try:
# Check if the file exists and load existing data
if os.path.exists(DNS_OUTPUT_FILE):
with open(DNS_OUTPUT_FILE, "r") as f:
try:
existing_data = json.load(f)
except json.JSONDecodeError:
existing_data = {} # Handle empty or corrupted JSON file
else:
existing_data = {}
# Update with the new record
existing_data.update(data)
# Write back to the file
with open(DNS_OUTPUT_FILE, "w") as f:
json.dump(existing_data, f, indent=4, ensure_ascii=False) # indent for readability, disable ASCII escaping
print(f"DNS record written to {DNS_OUTPUT_FILE}")
except Exception as e:
print(f"Error writing DNS record to file: {e}")
if __name__ == "__main__":
import sys
# Backup Amavis config *before* processing any domains
if not backup_amavis_config():
print("Failed to backup Amavis config. Aborting.")
sys.exit(1)
try:
with open("domains.txt", "r") as f:
domains = [line.strip() for line in f.readlines()]
except FileNotFoundError:
print("Error: domains.txt not found")
sys.exit(1)
for domain in domains:
print(f"Processing domain: {domain}")
if domain_exists(domain):
print(f"Domain {domain} already exists in Amavis config. Skipping.")
continue
key_path = generate_dkim_key(domain)
if key_path:
if update_amavis_config(domain, key_path):
public_key = get_dkim_public_key(key_path)
if public_key:
write_dns_record(domain, public_key)
else:
print(f"Failed to get public key for {domain}")
else:
print(f"Failed to update amavis config for {domain}")
if domains: #Only restart if there was at least one domain to process
restart_amavis()
演示视频:https://www.bilibili.com/video/BV1Jj7fztEN3/
参考:
环境:Ubunt 24.04,iredmail 1.7.1 ,安装时,勾选了 SOGo 组件,因为需要使用它的 CalDAV 和 CardDAV 服务
步骤:
vi /etc/nginx/templates/sogo.tmpl # 修改 sogo.tmpl 的文件内容,注释或删除原 SOGo 的 Microsoft-Server-ActiveSync 内容,用新的 z-push 替换:
location ^~ /Microsoft-Server-ActiveSync {
proxy_pass http://127.0.0.1:20000/SOGo/Microsoft-Server-ActiveSync;
proxy_connect_timeout 3540;
proxy_send_timeout 3540;
proxy_read_timeout 3540;
proxy_busy_buffers_size 64k;
proxy_buffers 8 64k;
proxy_buffer_size 64k;
}
location ^~ /SOGo/Microsoft-Server-ActiveSync {
proxy_pass http://127.0.0.1:20000/SOGo/Microsoft-Server-ActiveSync;
proxy_connect_timeout 3540;
proxy_send_timeout 3540;
proxy_read_timeout 3540;
proxy_busy_buffers_size 64k;
proxy_buffers 8 64k;
proxy_buffer_size 64k;
}
新的:
location /Microsoft-Server-ActiveSync {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
fastcgi_read_timeout 630;
include /etc/nginx/templates/fastcgi_php.tmpl;
client_max_body_size 128M;
}
location ~* ^/autodiscover/autodiscover.xml$ {
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
include /etc/nginx/templates/fastcgi_php.tmpl;
}
vi /usr/local/lib/z-push/backend/combined/config.php # 编辑混搭模式里的配置文件内容,将其中罗列的类型精简。如本例,只使用 BackendIMAP , BackendCardDAV 和 BackendCalDAV 。同时,要将目录的处理类型指定,避免出现指定不存在的 z 类型
public static function GetBackendCombinedConfig() {
//use a function for it because php does not allow
//assigning variables to the class members (expecting T_STRING)
return array(
//the order in which the backends are loaded.
//login only succeeds if all backend return true on login
//sending mail: the mail is sent with first backend that is able to send the mail
'backends' => array(
'i' => array(
'name' => 'BackendIMAP',
),
'd' => array(
'name' => 'BackendCardDAV',
),
'c' => array(
'name' => 'BackendCalDAV',
),
),
'delimiter' => '/',
//force one type of folder to one backend
//it must match one of the above defined backends
'folderbackend' => array(
SYNC_FOLDER_TYPE_INBOX => 'i',
SYNC_FOLDER_TYPE_DRAFTS => 'i',
SYNC_FOLDER_TYPE_WASTEBASKET => 'i',
SYNC_FOLDER_TYPE_SENTMAIL => 'i',
SYNC_FOLDER_TYPE_OUTBOX => 'i',
SYNC_FOLDER_TYPE_TASK => 'c',
SYNC_FOLDER_TYPE_APPOINTMENT => 'c',
SYNC_FOLDER_TYPE_CONTACT => 'd',
SYNC_FOLDER_TYPE_NOTE => 'c',
SYNC_FOLDER_TYPE_JOURNAL => 'c',
SYNC_FOLDER_TYPE_OTHER => 'i',
SYNC_FOLDER_TYPE_UNKNOWN => 'i',
),
//creating a new folder in the root folder should create a folder in one backend
'rootcreatefolderbackend' => 'i',
);
}
参考:
需求:有一个简单的 web 表单,让未登录的访客 注册 邮箱
思路:iredadmin 里,有 Create 的类,照它样子,复制出一个 Register 类来;同样,模仿着添加好 web 访问的路径 和 html 表单
环境:debian 12 里安装好的 iredmail - 1.7.1 SQL 版本
请注意,我不会 Python ,这是我在 Poe GPT 帮助下,简单制作的,仅用于测试。
步骤:
文件内容:iRedAdmin-2.6/controllers/sql/urls.py 中,添加 访问路径 :
'/register', 'controllers.sql.basic.Register',
文件内容:iRedAdmin-2.6/controllers/sql/basic.py 中,添加 Register 的类 :
from libs import iredpwd
from libs.sqllib import general as sql_lib_general
class Register:
def GET(self):
available_domains = ['iredmail.demo.anqun.org', 'imap.demo.2xinxian.top', 'example.net'] # 示例域名列表
form = web.input()
return web.render(
'register.html',
available_domains=available_domains,
msg=form.get('msg'),
)
def POST(self):
form = web.input()
domain = form.get('domainName', '').strip().lower()
username = form.get('username', '').strip().lower()
newpw = form.get('newpw', '').strip()
confirmpw = form.get('confirmpw', '').strip()
cn = form.get('cn', '').strip() # 获取可选的显示名称
if not username or not newpw or not confirmpw or not domain:
return web.seeother(f'/register?msg=INVALID_INPUT')
if newpw != confirmpw:
return web.seeother(f'/register?msg=PASSWORDS_DO_NOT_MATCH')
# 生成密码哈希
pwscheme = 'SSHA512'
passwd_hash = iredpwd.generate_password_hash(newpw, pwscheme=pwscheme)
result = self.add_user(domain, username, passwd_hash, cn) # 将 cn 传递给 add_user
if result[0]:
return web.seeother(f'/register?msg=CREATED')
else:
return web.seeother(f'/register?msg={web.urlquote(result[1])}')
def add_user(self, domain, username, password, cn):
mail = f"{username}@{domain}"
if sql_lib_general.is_email_exists(mail):
return (False, 'ALREADY_EXISTS')
record = {
'domain': domain,
'username': mail,
'password': password,
'active': 1,
'name': cn
}
try:
_wrap = SQLWrap()
conn = _wrap.conn
conn.insert('mailbox', **record)
return (True, )
except Exception as e:
return (False, repr(e))
创建模板文件 iRedAdmin-2.6/templates/default/register.html
{% extends "layout.html" %}
{% block title %}{{ _('Add mail user') }}{% endblock title %}
{% block navlinks_create %}class="active"{% endblock %}
{% block main %}
{# Show system message #}
{% if msg %}
{% if msg.startswith('PW_') %}
{% set _pw_errors = msg.split(',') %}
{% for _err in _pw_errors %}
{{ user_msg_handler(_err) }}
{% endfor %}
{% else %}
{# {{ user_msg_handler(msg) }} #}
<p>{{ msg }}</p>
{% endif %}
{% endif %}
<div class="content-box">
<div class="box-body">
<div class="box-header clear">
<ul class="tabs clear">
<li class="active"><a href="#user_add"><i class="fa fa-plus"></i> {{ _('User') }}</a>
</ul>
<h2>{{ _('Add mail user') }}</h2>
</div>
<div id="user_add" class="box-wrap clear">
<form name="form_add_user" method="post" action="{{ctx.homepath}}/register">
<div class="form-field clear">
<h4 class="size-250 fl-space">{{ _('Mail Domain') }} <span class="required">*</span></h4>
<span class="clean-padding">
<select name="domainName" id="domainSelect" required>
{% for domain in available_domains %}
<option value="{{ domain }}">{{ domain }}</option>
{% endfor %}
</select>
</span>
</div>
<div class="form-field clear">
<h4 class="size-250 fl-space">{{ _('Mail Address') }} <span class="required">*</span></h4>
<span class="clean-padding">
<input type="text" size="35" name="username" value="" autocomplete="off" class="text fl-space" required />@
</span>
</div>
<div class="form-field clear">
<h4 class="size-250 fl-space">{{ _('Password') }} <span class="required">*</span></h4>
<span class="clean-padding">
<input type="password" name="newpw" required class="text fl-space" />
</span>
</div>
<div class="form-field clear">
<h4 class="size-250 fl-space">{{ _('Confirm Password') }} <span class="required">*</span></h4>
<span class="clean-padding">
<input type="password" name="confirmpw" required class="text fl-space" />
</span>
</div>
<div class="form-field clear">
<h4 class="size-250 fl-space">{{ _('Display Name (Optional)') }}</h4>
<span class="clean-padding">
<input type="text" name="cn" value="" autocomplete="off" class="text fl-space" />
</span>
</div>
<div class="form-field clear">
<span>
<input type="submit" name="submit_add_user" value="{{ _('Add') }}" class="button green"/>
</span>
</div>
</form>
</div>{# -- End box-wrap -- #}
</div>{# -- End content-box -- #}
</div>{# -- End box-body -- #}
{% endblock main %}
参考:
环境和已下载的文件,请看上一篇。继续安装插件 kolab_2fa 。
步骤:
参考:https://git.kolab.org/diffusion/RPK/browse/master/plugins/kolab_2fa/