Mox: poštovní server v jednom souboru, který sám vyřeší všechny složitosti

27. 5. 2025
Doba čtení: 9 minut

Sdílet

Poštovní schránka před rodinným domem
Autor: Root.cz s využitím Zoner AI
Svět elektronické pošty míří k centralizaci. Částečně i proto, že spustit a udržovat vlastní poštovní server je velmi náročné. Zajímavým řešením je Mox, který všechny technické detaily vyřeší za vás.

Jak se pošta centralizuje

Co se dozvíte v článku
  1. Jak se pošta centralizuje
  2. Co je Mox a proč je zajímavý
  3. Co všechno Mox řeší
  4. Praktický popis instalace a zprovoznění
  5. Zkušenosti s nasazením
  6. Praktické zkušenosti
  7. Výhled do budoucna

Provozovat v dnešní době vlastní poštovní server je čím dál náročnější. Neustálé řešení, proč někdo odmítá přijímat vaše zprávy, je kolikrát nad síly lidí, kteří mají poštu na starosti. Často tak dochází k centralizaci služeb u velkých poskytovatelů, jako jsou Google Workspace, či Microsoft 365.

Lidé si myslí, že když za službu platí, nic se jim nemůže stát. Ale věděli jste, že Google zablokuje libovolnou schránku na svém serveru na 24 hodin, pokud překročíte nastavené limity? Ty navíc nelze  navýšit bez ohledu na verzi, kterou používáte. Po uplynutí 24 hodin dojde k obnově limitů, ale zprávy poslané během doby blokace se už neobnoví a odesílatel je bude muset poslat znova.

Pokud se někdo odvážný pokusí zprovoznit svůj poštovní server, brzy zjistí, že je potřeba udržovat veliké množství rozličných programů propojených různými protokoly. K posílání zpráv slouží například Postfix, k uživatelskému přístupu pak Dovecot, generování podpisů zajistí OpenDKIM a tak podobně.

Pak tu máme ještě zabezpečení DNS, obnovu certifikátů, koukání do logů a podobné problémy. Třeba se vám sice automaticky obnovil certifikát, ale Dovecot o tom neví a mnoho podobných otravných provozních problémů.

Co je Mox a proč je zajímavý

V roce 2021 začal být nizozemský softwarový inženýr na volné noze Mechiel Lukkien nespokojený se situací a začal dělat na svém projektu Mox. Vadilo mu, že většina poštovních serverů má svoje komponenty napsané v jazyce C (kam spadají výše zmíněné Postfix i Dovecot) a dle jeho slov tak může malinkatá chyba způsobit obrovské následky.

Kód je navíc velmi složitý, jak na sebe programy nabalují nové funkce a protokoly, jak se postupně vyvíjelo zabezpečení či doručování pošty. Tak začal svůj projekt psát v jazyce Go. První verze vyšla v roce 2023 a od té doby autor neustále Mox zlepšuje, opravuje a přidává nové funkce.

Zatím poslední verze 0.0.15 vyšla 18. dubna 2025. Nízké číslo verze může budit obavy, ale server je použitelný a stabilní, byť zatím málo rozšířený.

Co všechno Mox řeší

Mox je bezpečný poštovní server, který implementuje všechny moderní požadavky do jednoho souboru, který se snadno používá a udržuje. Zdrojový kód je vydán pod licencí MIT. Na loňskéletošní konferenci FOSDEM měl autor přednášku. Na letošním InstallFestu měl o Moxu přednášku Radomír Polách, její shrnutí si můžete přečíst v článku Poštovní server jednoduše, otevřený kryptočip a vlastní NAS, InstallFest 2025 či se můžete podívat na záznam.

Mox podporuje všechny moderní záležitosti, co se zabezpečení poštovních serverů týče: DKIM, DMARC, DANE, SPF či EAI. Ve spolupráci s ACME a Let’s Encrypt zvládá vygenerování či obnovu certifikátů, dále obsahuje ochranu proti spamu a vestavěný webový server. Nemá problém s výkonem, Radomír Polách na své přednášce řekl, že ho provozuje doma na Raspberry Pi.

Rozhraní poštovního serveru Mox

 Jednoduché rozhraní pro správce

Praktický popis instalace a zprovoznění

Instalace je velice velmi jednoduchá, stačí si ze stránek xmox.nl stáhnout binární soubor a spustit ho. Zatím není součástí běžných balíčkovacích služeb jednotlivých distribucí, ale výhoda binárních souborů v Go je jejich statická kompilace, kdy je možné je provozovat v Dockeru, či jako službu systemd.

Po stažení stačí jen soubor o zhruba velikosti 40 MB označit jako spustitelný a jednoduchým příkazem ho spustit.

# ./mox quickstart [email protected]

Mox hned sám vygeneruje vše potřebné, jako jsou odpovídající záznamy do DNS, definice služby v systemd, vygeneruje heslo administrátora a poradí, jak se pomocí SSH připojit na lokální webový server. Vše se uloží do souboru quickstart.log. Autor by chtěl tento proces do budoucna zautomatizovat.

# ssh -L 8080:localhost:80 you@yourmachine

V adresáři s Moxem se vytvoří dva nové podadresáře. Ten s názvem config obsahuje klíče DKIM, haš hesla administrátora a konfigurační soubory. Další je adresář  data, ve kterém se nacházejí jednotlivé zprávy uživatelů, certifikát Let's Encrypt, fronta zpráv, složka pro dočasné soubory a nejrůznější databáze nutné pro vlastní chod poštovního serveru.

Rozhraní poštovního serveru Mox

Integrované webové rozhraní pro práci s poštou 

Zkušenosti s nasazením

Pokud na serveru již běží stávající webový server, lze Mox nastavit tak, aby mu původní server dělal reverzní proxy. Příkaz pro to je:

# mox quickstart -existing-webserver

V případě, že si nejste jisti, zda všechno poběží, jak má, dá se Mox vyzkoušet i lokálně příkazem mox localserve. V takovém případě Mox spustí lokální instanci, která běží na portech vyšších o 1000, takže web na místo portu 80 běží na portu 1080, smtp místo 25 na 1025 a tak podobně.

Ukázka záznamů DNS

V následujících ukázkách si předvedeme, jak vypadají jednotlivé záznamy DNS vygenerované pro doménu example.com

; Outgoing messages will be signed with the first two DKIM keys. The other two
; configured for backup, switching to them is just a config change.
2025a._domainkey.example.com.   TXT "v=DKIM1;h=sha256;k=ed25519;p=bRGAIq+E5Hl0Pdd1V6P4Xa9ScqJLAQb5AUyL6BcfksI="
; NOTE: The following is a single long record split over several lines for use
; in zone files. When adding through a DNS operator web interface, combine the
; strings into a single string, without ().
2025b._domainkey.example.com.   TXT (
                "v=DKIM1;h=sha256;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEpNvv5TDyOPajgm2K/v0HR+S3B24ap2Jyh5X"
                "g+Mn3DiwvLhbHxQVIMxWLtZ5VkpVSRLFoART/Dam6VUxyhsRtvzf28yUEn2sQ9qpYGWdgRxIcvloX24GmnZBMu9TTlpOJd3p99SE"
                "E+CTQ9GjbIuH2jKn2q4qhGiioKUsX8WPRYMhu5DGts6rmnDbWraw2TE6iiMMOXx2I4bSguw7vLs1VmCmYI5EpeHKL3919LMu5c8/"
                "NdIQ9zockFB6VgrKNo9U3ByooMqZ/lJOvBi5XOa1wBrZFlcTNr0sPIlnitadTihZCr0SEH8Afs4PAHMT6sptLqZRzeQDCzu5HKTn"
                "AFWrQIDAQAB"
        )
2025c._domainkey.example.com.   TXT "v=DKIM1;h=sha256;k=ed25519;p=BQmuJChM9gnDsl+/FHM6V7u9JXzeAVyjDG5aNaQ1pQ0="
; NOTE: The following is a single long record split over several lines for use
; in zone files. When adding through a DNS operator web interface, combine the
; strings into a single string, without ().
2025d._domainkey.example.com.   TXT (
                "v=DKIM1;h=sha256;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApRBUIHPzM2RquLgptxjLR8sKpnNVjVd774NHs"
                "l74/bInKfp36rkbCyTmBDfzvt4u5v4GdBmZ0gm623B88MSKYlQlCGolIPEOsducx1OZPjBvnvtksw2kscy6Go3TqFaTWWIkYxMDX"
                "KMOcnV8DuoFHq5Kolnvb9+fGOI/fdh6PKHzYkPYb6/aDINJAM5GASqSD6CUyT2xCZASEhIZplGl8nYI1R+s2AVCsFzcCMn4zvFEZ"
                "bYIh0zEwAwf5ZipPcM1z8CORMbRU0vLQzfRpZXdtivu+fwc7SpBDSBltUPR33iXD5ZnbIie5pLf1p+UEqeO27R5XYsfWxHWXnBbk"
                "JwM6QIDAQAB"
        )
; Specify the MX host is allowed to send for our domain and for itself (for DSNs).
; ~all means softfail for anything else, which is done instead of -all to prevent older
; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.
example.com.                    TXT "v=spf1 ip4:23.215.0.136 ip6:2600:1406:3a00:21::173e:2e65 mx ~all"
; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)
; should be rejected, and request reports. If you email through mailing lists that
; strip DKIM-Signature headers and don't rewrite the From header, you may want to
; set the policy to p=none.
_dmarc.example.com.             TXT "v=DMARC1;p=reject;rua=mailto:[email protected]!10m"
; Remote servers can use MTA-STS to verify our TLS certificate with the
; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with
; STARTTLSTLS.
mta-sts.example.com.        CNAME a23-215-0-136.deploy.static.akamaitechnologies.com.
_mta-sts.example.com.           TXT "v=STSv1; id=20250429T121032"
; Request reporting about TLS failures.
_smtp._tls.example.com.         TXT "v=TLSRPTv1; rua=mailto:[email protected]"
Výsledek testu pošty odeslané ze serveru Mox

Výsledek testu připravenosti poštovního serveru 

Aktualizace na novou verzi

Když vyjde nová verze, je sice aktualizace jednoduchá, ale použití menšího množství příkazů se nelze vyhnout. Postupně si je projdeme.

Prvním příkazem se vytvoří dočasná záloha:

$ mox[stará verze] backup data/tmp/testupgrade

Druhým se ověří ve staré verzi Moxe, jestli je záloha konzistentní a v pořádku:

$ mox[stará verze] verifydata data/tmp/testupgrade
data/tmp/testupgrade: OK

Pak je možné ověřit zálohu i v nové verzi:

$ mox[nová verze] verifydata data/tmp/testupgrade
NOTE: The backup was made with mox version "v0.0.14-go1.24.2", while verifydata was run with mox version "v0.0.15-go1.24.2". Database files have probably been modified by running mox verifydata. Make a fresh backup before upgrading.
data/tmp/testupgrade: OK

Pokud vše úspěšně projde, stačí opět zazálohovat soubory, následně přepsat starou verzi tou novou a restartovat poštovní server. Při předchozí kontrole dochází k přepsání zálohovaných dat a ty pak nelze použít v nové verzi. V budoucnu by chtěl autor tento postup zjednodušit tak, aby byl na kliknutí ve webovém rozhraní a vše se udělalo samo.

Pokud je potřeba při aktualizaci v konfiguraci něco změnit, je o tom v poznámkách k vydání podrobný zápis. Mox si umí vydání nové verze sám pohlídat. Pokud je v konfiguraci nastaveno CheckUpdates na true, Mox jednou denně zkontroluje pomocí DNS dotazu na TXT záznam v doméně _updates.xmox.nl dostupnost nové verze. Pokud zjistí, že nová verze je k dispozici, dá vědět správci do poštovní schránky.

_updates.xmox.nl.       1200    IN      TXT     "v=UPDATES0;l=v0.0.15"
Rozhraní poštovního serveru Mox

 Oznámení o nové verzi serveru Mox

Praktické zkušenosti

Jednou z výhod Moxe je, že je součástí také webový server s poštovním rozhraním. Vypadá sice trochu spartánsky, ale je plně použitelný. Dokonce podporuje i takové srandy, jako je EAI. Pro snazší provoz s poštovními klienty si Mox vygeneruje potřebné záznamy do DNS. Ty pak vypadají takto:

; Client settings will reference a subdomain of the hosted domain, making it
; easier to migrate to a different server in the future by not requiring settings
; in all clients to be updated.
mail.example.com.               CNAME a23-215-0-136.deploy.static.akamaitechnologies.com.

; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.
autoconfig.example.com.         CNAME a23-215-0-136.deploy.static.akamaitechnologies.com.
_autodiscover._tcp.example.com. SRV 0 1 443 a23-215-0-136.deploy.static.akamaitechnologies.com.

; For secure IMAP and submission autoconfig, point to mail host.
_imaps._tcp.example.com.        SRV 0 1 993 a23-215-0-136.deploy.static.akamaitechnologies.com.
_submissions._tcp.example.com.  SRV 0 1 465 a23-215-0-136.deploy.static.akamaitechnologies.com.

Mox samozřejmě použije správný reverzní záznam adresy, kde běží. Do DNS je možné přidat i záznamy o nepoužívání protokolů POP3 a nezabezpečené verze IMAP. Také lze využít CAA záznam, který říká, že certifikáty pro danou doménu budou vystavovány výhradně pomocí autority Letʼs Encrypt.

; Next records specify POP3 and non-TLS ports are not to be used.
; These are optional and safe to leave out (e.g. if you have to click a lot in a
; DNS admin web interface).
_imap._tcp.example.com.         SRV 0 0 0 .
_submission._tcp.example.com.   SRV 0 0 0 .
_pop3._tcp.example.com.         SRV 0 0 0 .
_pop3s._tcp.example.com.        SRV 0 0 0 .

; Optional:
; You could mark Let's Encrypt as the only Certificate Authority allowed to
; sign TLS certificates for your domain.
example.com.                    CAA 0 issue "letsencrypt.org"
;
; Note: After starting up, once an ACME account has been created, CAA records
; that restrict issuance to the account will be suggested.

administraci lze dělat spoustu věcí. Od zakládání uživatelských účtů, nastavování aliasů, až po různé nastavení filtrů pro nevyžádanou poštu. Rozhraní by si zasloužilo trochu graficky doladit, třeba pomocí lepších kaskádových stylů, ale dá se zvyknout i na to stávající.

Pokud někomu nevyhovuje vertikální rozdělení, kdy vlevo je seznam zpráv a vpravo okno pro čtení, lze webové rozhraní přepnout do režimu nahoře seznam a dole okno pro čtení, či nastavit, aby se zprávy řadily do vláken.

Rozhraní poštovního serveru Mox

 Nastavení poštovního účtu

Výhled do budoucna

Autora podle vlastních slov čeká ještě hodně práce. Chce se věnovat neustálému vylepšování a zdokonalování Moxe. V plánu má přidat kalendář, automatické odpovědi (mimo kancelář), použití záložního MX serveru, jednoduchého průvodce prvním spuštěním krok za krokem a další funkce.

zabbix_tip

Co naopak zatím v plánu není, je podpora POP3, SMTP relay či push notifikací pro iOS. Autor ale nevylučuje, že se to nemůže v budoucnu změnit.

(Autorem obrázků je Tomáš Tichý.)

Autor článku

Milovník chameleonů, ajťák, šotouš, amatérský fotograf a hrdý řiditel autobusu Karosa.

'; document.getElementById('outstream-iframe').onload = function () { setupIframe(); } replayScreen = document.getElementById('iinfoOutstreamReplay'); iinfoOutstreamPosition = document.getElementById('iinfoOutstreamPosition'); outstreamContainer = document.getElementsByClassName('outstream-container')[0]; setupReplayScreen(); } function setupIframe() { outstreamDocument = document.getElementById('outstream-iframe').contentWindow.document; let el = outstreamDocument.createElement('style'); outstreamDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:-5px;right:25px}"; videoContent = outstreamDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; if ( location.href.indexOf('rejstriky.finance.cz') !== -1 || location.href.indexOf('finance-rejstrik') !== -1 || location.href.indexOf('firmy.euro.cz') !== -1 || location.href.indexOf('euro-rejstrik') !== -1 || location.href.indexOf('/rejstrik/') !== -1 || location.href.indexOf('/rejstrik-firem/') !== -1) { outstreamDirectPlayed = true; soundAllowed = true; iinfoVastUrlIndex = 0; } if (!outstreamDirectPlayed) { console.log('OUTSTREAM direct'); setUpIMA(true); } else { if (soundAllowed) { const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('OUTSTREAM sound allowed'); setUpIMA(false); }).catch(function () { console.log('OUTSTREAM sound forbidden'); renderBanner(); }); } } else { renderBanner(); } } } function getWrapper() { let articleWrapper = document.querySelector('.rs-outstream-placeholder'); // Outstream Placeholder from RedSys manipulation if (articleWrapper && articleWrapper.style.display !== 'block') { articleWrapper.innerHTML = ""; articleWrapper.style.display = 'block'; } // Don't render OutStream on homepages if (articleWrapper === null) { if (document.querySelector('body.p-index')) { return null; } } if (articleWrapper === null) { articleWrapper = document.getElementById('iinfo-outstream'); } if (articleWrapper === null) { articleWrapper = document.querySelector('.layout-main__content .detail__article p:nth-of-type(6)'); } if (articleWrapper === null) { // Euro, Autobible, Zdravi articleWrapper = document.querySelector('.o-article .o-article__text p:nth-of-type(6)'); } if (articleWrapper === null) { articleWrapper = document.getElementById('sidebar'); } if (!articleWrapper) { console.error("Outstream wrapper of article was not found."); } return articleWrapper; } function setupDimensions() { outstreamWidth = Math.min(iinfoOutstreamPosition.offsetWidth, 480); outstreamHeight = Math.min(iinfoOutstreamPosition.offsetHeight, 320); } /** * Sets up IMA ad display container, ads loader, and makes an ad request. */ function setUpIMA(direct) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); if (direct) { adsRequest.adTagUrl = directVast; console.log('Outstream DIRECT CAMPAING advert: ' + directVast); videoContent.muted = true; videoContent.volume = 0; outstreamDirectPlayed = true; } else { adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Outstream advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; } // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = outstreamWidth; // adsRequest.linearAdSlotHeight = outstreamHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function setupReplayScreen() { replayScreen.addEventListener('click', function () { iinfoOutstreamPosition.remove(); iinfoVastUrlIndex = 0; outstreamInit(); }); } /** * Sets the 'adContainer' div as the IMA ad display container. */ function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. outstreamDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( outstreamDocument.getElementById('adContainer'), videoContent); } function unmuteAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } } /** * Loads the video content and initializes IMA ad playback. */ function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(outstreamWidth, outstreamHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } /** * Handles the ad manager loading and sets ad event listeners. * @param { !google.ima.AdsManagerLoadedEvent } adsManagerLoadedEvent */ function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } /** * Handles actions taken in response to ad events. * @param { !google.ima.AdEvent } adEvent */ function onAdEvent(adEvent) { // Retrieve the ad from the event. Some events (for example, // ALL_ADS_COMPLETED) don't have ad object associated. const ad = adEvent.getAd(); console.log('Outstream event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to // determine whether the ad is a video ad or an overlay. if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. // Use ad.width and ad.height. videoContent.play(); } outstreamDocument.getElementById('adContainer').style.width = '100%'; outstreamDocument.getElementById('adContainer').style.maxWidth = '640px'; outstreamDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); // This event indicates the ad has started - the video player // can adjust the UI, for example display a pause button and // remaining time. if (ad.isLinear()) { // For a linear ad, a timer can be started to poll for // the remaining time. intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } outstreamDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (outstreamLastError === 303) { if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } } break; case google.ima.AdEvent.Type.COMPLETE: // This event indicates the ad has finished - the video player // can perform appropriate UI actions, such as removing the timer for // remaining time detection. if (ad.isLinear()) { clearInterval(intervalTimer); } if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } break; } } /** * Handles ad errors. * @param { !google.ima.AdErrorEvent } adErrorEvent */ function onAdError(adErrorEvent) { // Handle the error logging. console.log(adErrorEvent.getError()); outstreamLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { renderBanner(); } } function renderBanner() { if (isBanner) { console.log('Outstream: Render Banner'); iinfoOutstreamPosition.innerHTML = ""; iinfoOutstreamPosition.style.height = "330px"; iinfoOutstreamPosition.appendChild(bannerDiv); } else { console.log('Outstream: Banner is not set'); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoOutstreamPosition.remove(); outstreamInit(); } else { return false; } adVolume = 1; return true; } /** * Pauses video content and sets up ad UI. */ function onContentPauseRequested() { videoContent.pause(); // This function is where you should setup UI for showing ads (for example, // display ad timer countdown, disable seeking and more.) // setupUIForAds(); } /** * Resumes video content and removes ad UI. */ function onContentResumeRequested() { videoContent.play(); // This function is where you should ensure that your UI is ready // to play content. It is the responsibility of the Publisher to // implement this function when necessary. // setupUIForContent(); } function onActiveView() { if (outstreamContainer) { const containerOffset = outstreamContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (outstreamPaused) { adsManager.resume(); outstreamPaused = false; } return true; } else { if (!outstreamPaused) { adsManager.pause(); outstreamPaused = true; } } } return false; } let outstreamInitInterval; if (typeof cpexPackage !== "undefined") { outstreamInitInterval = setInterval(tryToInitializeOutstream, 100); } else { const wrapper = getWrapper(); if (wrapper) { let outstreamInitialized = false; window.addEventListener('scroll', () => { if (!outstreamInitialized) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { outstreamInit(); outstreamInitialized = true; } } }); } } function tryToInitializeOutstream() { const wrapper = getWrapper(); if (wrapper) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { if (cpexPackage.adserver.displayed) { clearInterval(outstreamInitInterval); outstreamInit(); } } } else { clearInterval(outstreamInitInterval); } } }
OSZAR »