[Web][Dovecot] Rework imapsync: sync sources, OAuth2 (XOAUTH2), worker pool, Python runner

This commit is contained in:
FreddleSpl0it committed 2026-10-22 10:07:52 +02:00
1 parent e756b7857e
commit 7c62a57910
39 files changed
+3582 -1358

No files matched your search

@@ -0,0 +1,177 @@
<!-- add imapsync source modal (shared between user and admin views) -->
<div class="modal fade" id="addImapsyncSourceModal" tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby="addImapsyncSourceModalLabel">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="addImapsyncSourceModalLabel">{{ lang.syncjobs.source_add }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lang.footer.cancel }}"></button>
</div>
<div class="modal-body">
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_imapsync_source">
<input type="hidden" value="0" name="active">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_name">{{ lang.syncjobs.source_name }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsrc_name" name="name" maxlength="100" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_description">{{ lang.syncjobs.source_description }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsrc_description" name="description" maxlength="255">
</div>
</div>
{% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
<div class="imapsync-source-scope-block">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_scope">{{ lang.syncjobs.source_scope }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-scope" id="addsrc_scope" name="scope" data-width="100%">
{% if mailcow_cc_role == 'admin' %}
<option value="all">{{ lang.syncjobs.source_scope_all }}</option>
{% endif %}
<option value="domain">{{ lang.syncjobs.source_scope_domain }}</option>
<option value="user" selected>{{ lang.syncjobs.source_scope_user }}</option>
</select>
</div>
</div>
<div class="row mb-2 imapsync-source-domains-row" style="display:none;">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_domains">{{ lang.syncjobs.source_domains }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-domains" id="addsrc_domains" name="domains" data-live-search="true" data-width="100%" title="{{ lang.syncjobs.source_domains }}" multiple></select>
</div>
</div>
<div class="row mb-2 imapsync-source-users-row" style="display:none;">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_users">{{ lang.syncjobs.source_users }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-users" id="addsrc_users" name="users" data-live-search="true" data-width="100%" title="{{ lang.syncjobs.source_users }}" multiple></select>
</div>
</div>
</div>
<div class="row mb-2 imapsync-source-scope-cc-hint" style="display:none;">
<div class="offset-sm-2 col-sm-10">
<small class="text-muted"><i class="bi bi-lock"></i> {{ lang.syncjobs.source_scope_cc_hint }}</small>
</div>
</div>
{% endif %}
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_host">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsrc_host" name="host" maxlength="253" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_port">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" id="addsrc_port" name="port" min="1" max="65535" value="993" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_enc">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select id="addsrc_enc" name="enc" title="{{ lang.add.select }}" data-width="100%" required>
<option value="SSL" selected>SSL</option>
<option value="TLS">STARTTLS</option>
<option value="PLAIN">PLAIN</option>
</select>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_auth_type">{{ lang.syncjobs.source_auth_type }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-auth-type" id="addsrc_auth_type" name="auth_type" data-width="100%" required>
<option value="PLAIN" selected>PLAIN</option>
<option value="LOGIN">LOGIN</option>
<option value="CRAM-MD5">CRAM-MD5</option>
{% if mailcow_cc_role != 'user' %}
<option value="XOAUTH2">XOAUTH2 (OAuth2)</option>
{% endif %}
</select>
</div>
</div>
{% if mailcow_cc_role != 'user' %}
<fieldset class="imapsync-source-oauth-block" style="display:none;">
<legend class="fs-5">{{ lang.syncjobs.source_oauth_block }}</legend>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_flow">{{ lang.syncjobs.source_oauth_flow }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-oauth-flow" id="addsrc_oauth_flow" name="oauth_flow" data-width="100%">
<option value="client_credentials" selected>{{ lang.syncjobs.source_oauth_flow_app }}</option>
<option value="authorization_code">{{ lang.syncjobs.source_oauth_flow_user }}</option>
</select>
</div>
</div>
<div class="row mb-2 imapsync-source-authcode-block" style="display:none;">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_authorize_endpoint">{{ lang.syncjobs.source_authorize_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" id="addsrc_oauth_authorize_endpoint" name="oauth_authorize_endpoint" placeholder="https://provider.example.com/oauth2/authorize">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_token_endpoint">{{ lang.syncjobs.source_oauth_token_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" id="addsrc_oauth_token_endpoint" name="oauth_token_endpoint" placeholder="https://provider.example.com/oauth2/token">
</div>
</div>
<div class="imapsync-source-authcode-block" style="display:none;">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_userinfo_endpoint">{{ lang.syncjobs.source_userinfo_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" id="addsrc_oauth_userinfo_endpoint" name="oauth_userinfo_endpoint" placeholder="https://provider.example.com/oauth2/userinfo">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_redirect_uri">{{ lang.syncjobs.source_redirect_uri }}</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="addsrc_redirect_uri" readonly value="{{ imapsync_redirect_uri }}" data-formcache-skip>
<button type="button" class="btn btn-secondary imapsync-copy-btn" data-copy-target="addsrc_redirect_uri" title="{{ lang.syncjobs.copy }}"><i class="bi bi-clipboard" aria-hidden="true"></i> {{ lang.syncjobs.copy }}</button>
</div>
<small class="text-muted">{{ lang.syncjobs.source_redirect_uri_hint }}</small>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_client_id">{{ lang.syncjobs.source_oauth_client_id }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsrc_oauth_client_id" name="oauth_client_id">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_client_secret">{{ lang.syncjobs.source_oauth_client_secret }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="addsrc_oauth_client_secret" name="oauth_client_secret" autocomplete="new-password" data-formcache-skip>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_scope">{{ lang.syncjobs.source_oauth_scope }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsrc_oauth_scope" name="oauth_scope" placeholder="openid email offline_access">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end" for="addsrc_oauth_extra_params">{{ lang.syncjobs.source_oauth_extra_params }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsrc_oauth_extra_params" name="oauth_extra_params" placeholder='{"resource":"https://provider.example.com"}'>
<small class="text-muted">{{ lang.syncjobs.source_oauth_extra_params_hint }}</small>
</div>
</div>
</fieldset>
{% endif %}
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="active" checked> {{ lang.add.active }}</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="add_item" data-id="add_imapsync_source" data-api-url='add/syncjob_source' data-api-attr='{}' href="#">{{ lang.admin.add }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
+65 -61
View File
@@ -933,20 +933,20 @@
</div>
</div><!-- add domain alias modal -->
<!-- add sync job modal -->
<div class="modal fade" id="addSyncJobModalAdmin" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" id="addSyncJobModalAdmin" tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby="addSyncJobModalAdminLabel">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.add.syncjob }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h3 class="modal-title" id="addSyncJobModalAdminLabel">{{ lang.syncjobs.add }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lang.footer.cancel }}"></button>
</div>
<div class="modal-body">
<p class="text-muted">{{ lang.add.syncjob_hint }}</p>
<p class="text-muted">{{ lang.syncjobs.add_hint }}</p>
<form class="form-horizontal" data-cached-form="false" role="form" data-id="add_syncjob">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="username">{{ lang.add.username }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_username">{{ lang.syncjobs.user2_label }}</label>
<div class="col-sm-10">
<select data-live-search="true" name="username" title="{{ lang.add.select }}" required>
<select data-live-search="true" id="addsja_username" name="username" title="{{ lang.add.select }}" data-width="100%" required>
{% for mailbox in mailboxes %}
<option>{{ mailbox }}</option>
{% endfor %}
@@ -954,140 +954,142 @@
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="host1">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="host1" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="port1">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required>
<small class="text-muted">1-65535</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="user1">{{ lang.add.username }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="user1" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="password1">{{ lang.add.password }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password1" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="enc1">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select name="enc1" title="{{ lang.add.select }}" required>
<option value="SSL" selected>SSL</option>
<option value="TLS">STARTTLS</option>
<option value="PLAIN">PLAIN</option>
<label class="control-label col-sm-2 text-sm-end" for="addsja_source_id">{{ lang.syncjobs.source_select }}</label>
<div class="col-sm-10 imapsync-select-wrap">
<select class="imapsync-source-select" id="addsja_source_id" data-live-search="true" data-show-subtext="true" data-width="100%" name="source_id" title="{{ lang.syncjobs.source_select }}" required>
{# options are loaded via the API on modal open (js/site/imapsync.js) #}
</select>
<div class="imapsync-no-source d-none mt-2">
<small class="text-muted">{{ lang.syncjobs.no_sources }}</small>
{% if mailcow_cc_role == 'admin' or acl.syncjobs == 1 %}
<button type="button" class="btn btn-sm btn-success ms-2 imapsync-open-source-modal"><i class="bi bi-plus-lg" aria-hidden="true"></i> {{ lang.syncjobs.source_add }}</button>
{% endif %}
</div>
</div>
</div>
<div class="row mb-2 imapsync-user1-row">
<label class="control-label col-sm-2 text-sm-end" for="addsja_user1">{{ lang.syncjobs.user1_label }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsja_user1" name="user1" autocomplete="off" required>
</div>
</div>
<div class="row mb-2 imapsync-oauth-connect-row" style="display:none;">
<div class="offset-sm-2 col-sm-10">
<button type="button" class="btn btn-sm btn-primary imapsync-oauth-connect-btn"><i class="bi bi-box-arrow-up-right" aria-hidden="true"></i> {{ lang.syncjobs.syncjob_oauth_connect }}</button>
<span class="imapsync-oauth-connect-status ms-2" role="status" aria-live="polite"></span>
<div><small class="text-muted">{{ lang.syncjobs.oauth_hint }}</small></div>
</div>
</div>
<div class="row mb-4 password1-row">
<label class="control-label col-sm-2 text-sm-end" for="addsja_password1">{{ lang.syncjobs.password1_label }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="addsja_password1" name="password1" autocomplete="new-password" data-formcache-skip>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="mins_interval">{{ lang.add.mins_interval }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_mins_interval">{{ lang.syncjobs.mins_interval }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="mins_interval" min="1" max="43800" value="20" required>
<input type="number" class="form-control" id="addsja_mins_interval" name="mins_interval" min="1" max="43800" value="20" required>
<small class="text-muted">1-43800</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="subfolder2">{{ lang.edit.subfolder2|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_subfolder2">{{ lang.syncjobs.subfolder2|raw }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="subfolder2" value="">
<input type="text" class="form-control" id="addsja_subfolder2" name="subfolder2" value="">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="maxage">{{ lang.edit.maxage|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_maxage">{{ lang.syncjobs.maxage|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxage" min="0" max="32000" value="0">
<input type="number" class="form-control" id="addsja_maxage" name="maxage" min="0" max="32000" value="0">
<small class="text-muted">0-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="maxbytespersecond">{{ lang.edit.maxbytespersecond|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_maxbytespersecond">{{ lang.syncjobs.maxbytespersecond|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxbytespersecond" min="0" max="125000000" value="0">
<input type="number" class="form-control" id="addsja_maxbytespersecond" name="maxbytespersecond" min="0" max="125000000" value="0">
<small class="text-muted">0-125000000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="timeout1">{{ lang.edit.timeout1 }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_timeout1">{{ lang.syncjobs.timeout1 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600">
<input type="number" class="form-control" id="addsja_timeout1" name="timeout1" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="timeout2">{{ lang.edit.timeout2 }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_timeout2">{{ lang.syncjobs.timeout2 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600">
<input type="number" class="form-control" id="addsja_timeout2" name="timeout2" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="exclude">{{ lang.add.exclude }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsja_exclude">{{ lang.syncjobs.exclude }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
<input type="text" class="form-control" id="addsja_exclude" name="exclude" value="(?i)spam|(?i)junk">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end" for="custom_params">{{ lang.add.custom_params }}</label>
<label class="control-label col-sm-2 text-sm-end" id="addsja_custom_params_label">{{ lang.syncjobs.custom_params }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="custom_params" placeholder="--some-param=xy --other-param=yx">
<small class="text-muted">{{ lang.add.custom_params_hint }}</small>
<div class="imapsync-cp-editor" role="group" aria-labelledby="addsja_custom_params_label">
<div class="imapsync-cp-rows"></div>
<button type="button" class="btn btn-sm btn-secondary imapsync-cp-add"><i class="bi bi-plus-lg" aria-hidden="true"></i> {{ lang.syncjobs.custom_param_add }}</button>
</div>
<input type="hidden" name="custom_params" class="imapsync-cp-value" value="[]">
<small class="text-muted d-block mt-1">{{ lang.syncjobs.custom_params_hint }}</small>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.add.delete2duplicates }} (--delete2duplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.syncjobs.delete2duplicates }} (--delete2duplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.add.delete1 }} (--delete1)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.syncjobs.delete1 }} (--delete1)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.add.delete2 }} (--delete2)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.syncjobs.delete2 }} (--delete2)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.add.automap }} (--automap)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.syncjobs.automap }} (--automap)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.add.skipcrossduplicates }} (--skipcrossduplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.syncjobs.skipcrossduplicates }} (--skipcrossduplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.add.subscribeall }} (--subscribeall)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.syncjobs.subscribeall }} (--subscribeall)</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="dry"> {{ lang.add.dry }} (--dry)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="dry"> {{ lang.syncjobs.dry }} (--dry)</label>
</div>
</div>
</div>
@@ -1347,3 +1349,5 @@
</div>
</div>
</div><!-- DNS info modal -->
{% include "modals/imapsync_source.twig" %}
+62 -58
View File
@@ -27,150 +27,153 @@
</div>
</div><!-- add domain admin modal -->
<!-- add sync job modal -->
<div class="modal fade" id="addSyncJobModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" id="addSyncJobModal" tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby="addSyncJobModalLabel">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.add.syncjob }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<h3 class="modal-title" id="addSyncJobModalLabel">{{ lang.syncjobs.add }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ lang.footer.cancel }}"></button>
</div>
<div class="modal-body">
<p>{{ lang.add.syncjob_hint }}</p>
<p>{{ lang.syncjobs.add_hint }}</p>
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob">
<div class="row mb-2">
<label class="control-label col-sm-2" for="host1">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="host1" required>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="port1">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required>
<small class="text-muted">1-65535</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="user1">{{ lang.add.username }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="user1" required>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="password1">{{ lang.add.password }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password1" data-hibp="true" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="enc1">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select name="enc1" title="{{ lang.add.select }}" required>
<option value="SSL" selected>SSL</option>
<option value="TLS">STARTTLS</option>
<option value="PLAIN">PLAIN</option>
<label class="control-label col-sm-2 text-sm-end" for="addsju_source_id">{{ lang.syncjobs.source_select }}</label>
<div class="col-sm-10 imapsync-select-wrap">
<select class="imapsync-source-select" id="addsju_source_id" data-live-search="true" data-show-subtext="true" data-width="100%" name="source_id" title="{{ lang.syncjobs.source_select }}" required>
{# options are loaded via the API on modal open (js/site/imapsync.js) #}
</select>
<div class="imapsync-no-source d-none mt-2">
<small class="text-muted">{{ lang.syncjobs.no_sources }}</small>
{% if mailcow_cc_role == 'admin' or acl.syncjobs == 1 %}
<button type="button" class="btn btn-sm btn-success ms-2 imapsync-open-source-modal"><i class="bi bi-plus-lg" aria-hidden="true"></i> {{ lang.syncjobs.source_add }}</button>
{% endif %}
</div>
</div>
</div>
<div class="row mb-2 imapsync-user1-row">
<label class="control-label col-sm-2 text-sm-end" for="addsju_user1">{{ lang.syncjobs.user1_label }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="addsju_user1" name="user1" autocomplete="off" required>
</div>
</div>
<div class="row mb-2 imapsync-oauth-connect-row" style="display:none;">
<div class="offset-sm-2 col-sm-10">
<button type="button" class="btn btn-sm btn-primary imapsync-oauth-connect-btn"><i class="bi bi-box-arrow-up-right" aria-hidden="true"></i> {{ lang.syncjobs.syncjob_oauth_connect }}</button>
<span class="imapsync-oauth-connect-status ms-2" role="status" aria-live="polite"></span>
<div><small class="text-muted">{{ lang.syncjobs.oauth_hint }}</small></div>
</div>
</div>
<div class="row mb-4 password1-row">
<label class="control-label col-sm-2 text-sm-end" for="addsju_password1">{{ lang.syncjobs.password1_label }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="addsju_password1" name="password1" autocomplete="new-password" data-formcache-skip data-hibp="true">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="mins_interval">{{ lang.add.mins_interval }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_mins_interval">{{ lang.syncjobs.mins_interval }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="mins_interval" min="1" max="43800" value="20" required>
<input type="number" class="form-control" id="addsju_mins_interval" name="mins_interval" min="1" max="43800" value="20" required>
<small class="text-muted">1-43800</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="subfolder2">{{ lang.edit.subfolder2|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_subfolder2">{{ lang.syncjobs.subfolder2|raw }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="subfolder2" value="">
<input type="text" class="form-control" id="addsju_subfolder2" name="subfolder2" value="">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="maxage">{{ lang.edit.maxage|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_maxage">{{ lang.syncjobs.maxage|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxage" min="0" max="32000" value="0">
<input type="number" class="form-control" id="addsju_maxage" name="maxage" min="0" max="32000" value="0">
<small class="text-muted">0-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="maxbytespersecond">{{ lang.edit.maxbytespersecond|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_maxbytespersecond">{{ lang.syncjobs.maxbytespersecond|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxbytespersecond" min="0" max="125000000" value="0">
<input type="number" class="form-control" id="addsju_maxbytespersecond" name="maxbytespersecond" min="0" max="125000000" value="0">
<small class="text-muted">0-125000000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="timeout1">{{ lang.edit.timeout1 }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_timeout1">{{ lang.syncjobs.timeout1 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600">
<input type="number" class="form-control" id="addsju_timeout1" name="timeout1" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="timeout2">{{ lang.edit.timeout2 }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_timeout2">{{ lang.syncjobs.timeout2 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600">
<input type="number" class="form-control" id="addsju_timeout2" name="timeout2" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="exclude">{{ lang.add.exclude }}</label>
<label class="control-label col-sm-2 text-sm-end" for="addsju_exclude">{{ lang.syncjobs.exclude }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
<input type="text" class="form-control" id="addsju_exclude" name="exclude" value="(?i)spam|(?i)junk">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="custom_params">{{ lang.add.custom_params }}</label>
<label class="control-label col-sm-2 text-sm-end" id="addsju_custom_params_label">{{ lang.syncjobs.custom_params }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="custom_params" placeholder="--delete2folders --otheroption">
<div class="imapsync-cp-editor" role="group" aria-labelledby="addsju_custom_params_label">
<div class="imapsync-cp-rows"></div>
<button type="button" class="btn btn-sm btn-secondary imapsync-cp-add"><i class="bi bi-plus-lg" aria-hidden="true"></i> {{ lang.syncjobs.custom_param_add }}</button>
</div>
<input type="hidden" name="custom_params" class="imapsync-cp-value" value="[]">
<small class="text-muted d-block mt-1">{{ lang.syncjobs.custom_params_hint }}</small>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.add.delete2duplicates }} (--delete2duplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.syncjobs.delete2duplicates }} (--delete2duplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.add.delete1 }} (--delete1)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.syncjobs.delete1 }} (--delete1)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.add.delete2 }} (--delete2)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.syncjobs.delete2 }} (--delete2)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.add.automap }} (--automap)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.syncjobs.automap }} (--automap)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.add.skipcrossduplicates }} (--skipcrossduplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.syncjobs.skipcrossduplicates }} (--skipcrossduplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.add.subscribeall }} (--subscribeall)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.syncjobs.subscribeall }} (--subscribeall)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="dry" checked> {{ lang.add.dry }} (--dry)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="dry" checked> {{ lang.syncjobs.dry }} (--dry)</label>
</div>
</div>
</div>
@@ -191,6 +194,7 @@
</div>
</div>
</div><!-- add sync job modal -->
{% include 'modals/imapsync_source.twig' %}
<!-- app passwd modal -->
<div class="modal fade" id="addAppPasswdModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg">