--[[---------------
Noki v0.4
-------------------
Noki is a toolkit to convert Nokia PC Suite backuped SMS to a
unicode .txt file, which is more accessible than the original
.nfb or .nfc.
It works well for Nokia PC Suite 6.5.12 and my mobile phone is
Nokia 7360. There might be some compatibility problem if you
use earlier version of the PC Suite.
How to use:
noki.save_sms('nokia.nfb', 'sms.txt')
Under the MIT license.
Noki is a part of LuaBit(http://luaforge.net/projects/bit/).
copyright(c) 2006~2007 hanzhao (abrash_han@hotmail.com)
--]]---------------
require 'hex'
require 'bit'
do
-- globals
local RETURN = '\13\0\10\0'
local SMS_FILE = '\\MPAPI\\MESSAGES'
local SMS_INBOX = 'PIT_MESSAGE_INBOX'
local SMS_OUTBOX = 'PIT_MESSAGE_OUTBOX'
local SMS_ARCHIVEBOX = 'PIT_MESSAGE_ARCHIVE'
-- output decorator
local SMS_INBOX_DEC = '[INBOX] '
local SMS_OUTBOX_DEC = '[OUTBOX] '
local SMS_ARCHIVE_DEC = '[ARCHIVE] '
-- box type
local BoxType = {
NON = 0,
IN = 1,
OUT = 2,
ARCHIVE = 3,
}
-- feed each char with an extra \0
local function asci_to_uni(asci)
--print("-------")
local uni = ""
for i = 1, string.len(asci) do
local str = string.format('%c\0', string.byte(asci, i))
--print(string.len(str))
uni = uni .. str
end
return uni
end
local function asci_padding(asci, pad)
local uni = ""
for i = 1, string.len(asci) do
local str = string.format('%c', string.byte(asci, i))
--print(string.len(str))
uni = uni .. str .. pad
end
return uni
end
-- shrink the \0 in uni code string
local function uni_to_asci(uni)
local asci = ''
--print('uni len ' .. string.len(uni))
for i = 1, string.len(uni), 2 do
asci = asci .. string.sub(uni, i, i)
end
return asci
end
local function reader(str)
local index = 1
return function (n)
--print('reader>>> idx ' .. index .. " n " .. n)
local sub = string.sub(str, index, index + n - 1)
--[[print(hex.to_hex(string.byte(sub, 1)))
print(hex.to_hex(string.byte(sub, 2)))
print(hex.to_hex(string.byte(sub, 3)))
print(hex.to_hex(string.byte(sub, 4)))
--]]
index = index + n
return sub
end
end
local function read_number(read, n)
local str = read(n)
local rslt = 0
for i = 1, n do
local v = string.byte(str, i)
rslt = bit.bor(rslt, bit.blshift(v, (i-1)*8))
end
return rslt
end
local function read_int(read)
return read_number(read, 4)
end
local function read_short(read)
return read_number(read, 2)
end
local function read_nfb_string(read)
local len = read_int(read)
local unistr = read(len*2)
return unistr
end
local function read_nfb_header(read)
local nfb_header = {
ver = read_int(read),
firmware = read_nfb_string(read),
phone = read_nfb_string(read),
entries = read_int(read),
}
return nfb_header
end
local function read_nfb_file(read)
local nfbf = {}
nfbf.path = read_nfb_string(read)
nfbf.nbytes = read_int(read)
nfbf.bytes = read(nfbf.nbytes)
local stamp = read_int(read)
return nfbf
end
local function read_nfb_dir(read)
local nfbd = {
path = read_nfb_string(read)
}
return nfbd
end
local function save_entry(fp, tbl)
for k, v in pairs(tbl) do
fp:write(v)
fp:write(RETURN)
end
end
-- save sms entries
local function save_sms(fp, ctnt)
-- print("save sms ----")
local in_box = asci_padding(SMS_INBOX, "%z")
local out_box = asci_padding(SMS_OUTBOX, "%z")
local archive_box = asci_padding(SMS_ARCHIVEBOX, "%z")
local line_s = asci_padding("1020", "%z")
local head = asci_padding("1033", "%z")
local tail = asci_padding("1040", "%z")
local service_center_tail = asci_padding("1080", "%z")
local phone_nb = "%+%z%d%z%d%z[%d%z]+" -- default is type 145 with '+'
local phone_nb_129 = string.rep("%d%z", 11) -- phone number type 129 without '+'
local time = "[%d%z]+%-%z%d%z%d%z%-%z%d%z%d%zT%z%d%z%d%z:%z%d%z%d%z"
local pattern = "([^\10]+)\13%z\10%z"
local line_end = "\13%z\10%z"
local lineb, linee = string.find(ctnt, line_end)
local start = 1
local line_number = 1
while(lineb and linee) do
local line = string.sub(ctnt, start, lineb - 1)
--line = string.sub(ctnt, gb, ge)
local type = BoxType.NON
--print('capture ' .. string.len(line))
--print(uni_to_asci(box))
if(string.find(line, in_box)) then
fp:write(asci_to_uni(SMS_INBOX_DEC))
type = BoxType.IN
elseif(string.find(line, out_box)) then
fp:write(asci_to_uni(SMS_OUTBOX_DEC))
type = BoxType.OUT
elseif(string.find(line, archive_box)) then
fp:write(asci_to_uni(SMS_ARCHIVE_DEC))
type = BoxType.ARCHIVE
else
--print(uni_to_asci(line))
io.close(fp)
--error('unknown sms type')
return
end
hb, he = string.find(line, head)
tb, te = string.find(line, tail)
local first_number = ""
-- service center address
sb, se = string.find(line, phone_nb, tb)
--print("" .. sb .. ", " .. se)
if(sb and se) then
--print(uni_to_asci(string.sub(line, sb, se)))
-- keep the find number, if the second find for sender address fails
-- then this number is the sender address
first_number = string.sub(line, sb, se)
else
sb, se = string.find(line, phone_nb_129, tb)
if(not (sb and se)) then
--io.close(fp)
--error("error service center address")
--return
first_number = "empty number"
-- nokia's pc suite may leave the serivce center address empty
end
end
-- sender address
se_old = se
sb, se = string.find(line, phone_nb, se)
--print("" .. sb .. ", " .. se)
local sender_address = ""
if(sb and se) then
--print(uni_to_asci(string.sub(line, sb, se)))
sender_address = string.sub(line, sb, se)
else
sb, se = string.find(line, phone_nb_129, se_old)
if(not (sb and se)) then
--[[
print(line_number)
io.close(fp)
error("error sender address")
--]]
sender_address = first_number
end
end
-- write sender
fp:write(sender_address)
fp:write(" \0")
-- date time
-- out box have no date time slot
if(type ~= BoxType.OUT and first_number ~= "empty number") then
tmb, tme = string.find(line, time, se)
--print('' .. tmb .. ", " .. tme)
if(tmb and tme) then
--print(uni_to_asci(string.sub(line, tmb+1, tme)))
else
io.close(fp)
error("error reading date time")
return
end
fp:write(string.sub(line, tmb+1, tme))
end
fp:write(RETURN)
fp:write(string.sub(line, he+3, tb-3))
fp:write(RETURN)
fp:write(RETURN)
--end
start = linee + 1
lineb, linee = string.find(ctnt, line_end, linee)
line_number = line_number + 1
end
end
-- save sms from a .nfc or .nfb file to a unicode .txt file
local function save_nfx_to(from, too)
local fp = io.open(too, 'wb')
if(not fp) then
error("error opening file " .. too .. " to write")
return
end
v = string.format('%c%c', 255, 254)
-- unicode .txt 'FF FE'
fp:write(v)
-- read the .nfc file
local nokia = io.open(from, 'rb')
if(not nokia) then
error("error open file " .. from .. " to read")
end
local ctnt = nokia:read("*a")
io.close(nokia)
local read = reader(ctnt)
local header = read_nfb_header(read)
--print(header.ver)
--print(header.entries)
for i=1, header.entries do
--print(i)
local type = read_int(read)
if(type == 1) then
-- file entry
--print('file')
local fe = read_nfb_file(read)
--save_entry(fp, fe)
if(uni_to_asci(fe.path) == SMS_FILE) then
local smsctnt = fe.bytes
--print('sms len ' .. fe.nbytes)
save_sms(fp, smsctnt)
return
end
elseif(type == 2) then
-- dir entry
--print('dir')
local fd = read_nfb_dir(read)
--save_entry(fp, fd)
else
io.close(fp)
error('unknown entry type : ' .. hex.to_hex(type))
end
end
io.close(fp)
end
-- noki interface --
noki = {
save_sms = save_nfx_to
}
end -- end block
-- sample
-- noki.save_sms('nokia2.nfb', 'sms2.txt')